20191224のReactに関する記事は14件です。

React で side effect のエラーハンドリング

この記事は CodeChrysalis Advent Calendar 2019 の記事です。

はじめに

ReactjsにはError Boundariesという、エラーをcatchしたときに専用のComponentをrenderしてくれる機能がありますが、これはrender時におけるエラーのみcatchする機能です。

componentDidCatchというライフサイクルが用意されていて、これを以てサードパーティのエラー監視パッケージに通知するようにと推奨されています。

ですが多くの場合、フロントエンドはバックエンドと通信する等のside effectを持ちます。

  1. side effectを用いたときのエラーハンドリングが必要となる
  2. side effectを用いたときもエラーメッセージを表示するComponentを共通化したい
  3. side effectを用いたときもサードパーティのエラー監視パッケージ(Sentryなど)に通知したい

という要件に対してのエラーハンドリング専用のComponentを考えました。

前提として、React Hooksを使います。

設計の内容

エラーが発生したときにContextを使ってエラーの情報を保存し、専用Componentでそれらの情報を使うようにしました。

コードの内容

Context

import React, { useState, useContext, createContext } from 'react';

const ErrorContext = createContext({
  hasError: false,
  userMessage: null,
  error: null,
  setContextError: (userMessage: string, error: Error) => {},
  setCotextErrorDone: () => {},
});

export const ErrorProvider: React.FC<object> = props => {
  const [hasError, setHasError] = useState<boolean>(false);
  const [userMessage, setUserMessage] = useState<string | null>(null);
  const [error, setError] = useState<Error | null>(null);

  const setContextError = (userMessage: string, error: Error) => {
    setUserMessage(userMessage);
    setError(error);
    setHasError(true);
  };

  const setCotextErrorDone = () => {
    setUserMessage(null);
    setError(null);
    setHasError(false);
  };

  return (
    <ErrorContext.Provider
      value={{ hasError, userMessage, error, setContextError, setCotextErrorDone }}
      {...props}
    />
  );
};

export const useError = () => {
  const context = useContext(ErrorContext);

  if (context === undefined) {
    throw new Error(`useError must be used within a ErrorProvider`);
  }
  return context;
};

要素の説明

  • hasError はこれがtrueになっているとErrorハンドリングのComponentをrenderするようにするためのstateです。
  • userMessage はエラーが発生した箇所でエラー内容が特定できるものに関してはその場でこのstateにエラーメッセージを保管し、ErrorハンドリングのComponentをrenderするときに使えるようにしています。
  • error はエラーが発生した箇所のtrycatchで取得したerrorインスタンスそのものを入れています。Sentryに送るためのものです。
  • setContextErrorでエラー発生時にエラー内容を保存し、hasErrorのstateを変更します。
  • setContextErrorDoneで、ErrorハンドリングのComponentからどこかに遷移するときにhasErrorのstateをfalseに変更するようにしています。
import React, { useState } from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import * as Sentry from '@sentry/browser';
import { Header } from 'components';
import { useUser } from 'context/UserContext';
import { useError } from 'context/ErrorContext';
import styles from './ErrorBoundary.module.css';

const ErrorBoundary: React.FC<RouteComponentProps> = ({ history: { push } }) => {
  const { userId } = useUser();
  const { hasError, userMessage, error, setCotextErrorDone } = useError();

  const [eventId, setEventId] = useState<string | null>(null);

  const handleHelp = () => {
    setCotextErrorDone();
    setEventId(null);

    push({ pathname: '/somewhere' });
  };

  if (hasError && !eventId) {
    Sentry.withScope(scope => {
      if (attendanceId) {
        scope.setUser({ id: userId });
      }
      scope.setExtras({ userMessage });
      scope.setTag('errorCategory', 'Side Effect');
      const currentEventId = Sentry.captureException(error);
      setEventId(currentEventId);
    });
  }

  let title = 'エラーが発生しました';

  if (hasError) {
    return (
      <aside className={styles.mainContainer}>
        <Header />
        <h1 className={styles.mainHeader}>{title}</h1>
        <h2 className={styles.subHeader}>
          <span>{userMessage}</span>
        </h2>
        <button className={styles.inquiryButton} onClick={handleHelp}>ヘルプ</button>
      </aside>
    );
  }
  return null;
};

export default withRouter(ErrorBoundary);

要素の説明

  • ErrorハンドリングのComponentを作成することで、Error時に表示するUIもカスタマイズで作成できます。
  • このComponentがrenderされたらSentryにメッセージを通知するようにしています。以下のコードの部分です。
    Sentry.withScope(scope => {
      if (attendanceId) {
        scope.setUser({ id: userId });
      }
      scope.setExtras({ userMessage });
      scope.setTag('errorCategory', 'Side Effect');
      const currentEventId = Sentry.captureException(error);
      setEventId(currentEventId);
    });
  • ヘルプボタンを押すとhandleHelpを実行して、setContextErrorDoneが実行され、エラーに関するContextがクリアされると同時にどこかのURIに移動するようにしています。
  • 別のContextであるuserUserからuserIdを取得して、Sentryに送信し、もし問い合わせが来たらSentry上のエラーメッセージと関連付けて調査できるようにしています(Sentry便利!)
  • let title = ... の部分はもしErrorの内容によってタイトルを変えたいときのためにletにしています。

Sentry

Sentryの設定で、Slackに送信するように連携しておけばこれらのメッセージをSlackに送信できます。

また、

scope.setUser({ id: userId });

これは任意のユーザー属性を割り当てることができます。

scope.setExtras({ userMessage });

これは補足情報を割り当てることができます。ここではユーザーにどのような情報を表示しているのかを把握するために割り当てています。

scope.setTag('errorCategory', 'Side Effect');

Sentryの画面にタグを表示できるので、React純粋のError BoundaryとこのカスタムのBoundaryを切り分けています。前者はrenderのエラー、後者はSide Effectで十中八九バックエンドか通信周りが関係していると悟ります。

さいごに

もしより良いエラーハンドリングの構造があればぜひ教えてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

これからはFunction Componentですべて解決できる――というのはどうやら幻想だったようです。

何がしたかったのか

Reactには、Lazy Componentというものがあります。

MyComponent.tsx
import React, { FC } from 'react';

const MyComponent: FC = () => (
  <div>Hello LazyComponent!</div>
);

export default MyComponent;
MyApp.tsx
import React, { FC, Suspense, lazy } from 'react';

const MyComponent = lazy(() => import('./MyComponent'));

const MyApp: FC = () => (
  <div>
    <Suspense fallback={<div>Loading...</div>}>
      <MyComponent />
    </Suspense>
  </div>
);

export default MyApp;

とすると、MyComponentのロードが完了するまでfallbackに設定された<div>Loading...</div>を代わりにレンダリングしてくれるというものです。

で、いろいろ調べてたらこんなこともできると判明。

LazyComponent.js
import React, { Component } from 'react';

let result = null;
const timeout = (msec) => new Promise(resolve => {
  setTimeout(resolve, msec)
});

const LazyComponent = () => {
  if (result !== null) {
    return (
      <div>{result}</div>
    )
  }
  throw new Promise(async(resolve) => {
    await timeout(1000);
    result = 'Done'
    resolve();
  })
};

export default LazyComponent;

React 16.6の新機能、React.lazyとReact.Suspense を使った非同期処理

こう書いたら、throwしたPromiseがresolveされたときにもう1回レンダリングされるらしく。私の探し方が悪いのか何なのか、この仕様はReactのドキュメント上で見つけることができませんでした。どこに書いてあるのか知っている人がいたらこっそり教えてほしいです。

それはさておきこの仕様、ドキュメントで見つからなかったので動かない前提で試しに書いてみました。

試しに書いたコード
import React, { FC, lazy, Suspense } from 'react';

const PromiseTest= lazy(async () => {
  let state = 0;
  const TestInner: FC = () => {
    if(state) {
      return (
        <div>Done! {state}</div>
      )
    }
    throw new Promise((res) => {
      setTimeout(() => {
        state = 5;
        res();
      }, 5000);
    });
  };
  return {
    default: TestInner,
  };
});

const TestApp: FC = () => {
  return (
    <div>
      <Suspense fallback={<div>WAITING...</div>}>
        <PromiseTest />
      </Suspense>
    </div>
  );
}

やってみた結果……動く!動くぞ!

さて、問題のコードに移ろうじゃないか

さて、Promiseをthrowしたら期待通りに動くことが分かったんですけれど。state = 5ってPromiseの中で変数に代入しちゃってるじゃないですか。ぶっちゃけキモいですよね。
useStateフックに置き換えてもいけるんじゃね?って思った私、置き換えてみました。

置き換えてみた
import React, { FC, lazy, Suspense, useState } from 'react';

const PromiseTest= lazy(async () => {
  const TestInner: FC = () => {
    const [state, setter] = useState(0);
    if(state) {
      return (
        <div>Done! {state}</div>
      )
    }
    throw new Promise((res) => {
      setTimeout(() => {
        setter(5);
        res();
      }, 5000);
    });
  };
  return {
    default: TestInner,
  };
});

const TestApp: FC = () => {
  return (
    <div>
      <Suspense fallback={<div>WAITING...</div>}>
        <PromiseTest />
      </Suspense>
    </div>
  );
}

あれ、動かん:thinking::thinking::thinking:
動かんぞ。

useStateに置き換える前は動いたコードが、置き換えた瞬間動かなくなりました。てゆうか、setterは普通に呼ばれているはずなのに、stateの値は0のまま。なんでや、、、。

諦めてComponent classにしてみた

というわけで、PromiseTestの実装をComponent classに変えてみました。statethis.state.statesetterthis.setStateに変えただけですけどね。

classに書き換えてみた
class TestInner extends React.Component<{}, { state: number }> {
  constructor(props: {}) {
    super(props);

    this.state = {
      state: 0,
    };
  }

  render() {
    if(this.state.state) {
      return (
        <li>Done! {this.state.state}</li>
      );
    }
    throw new Promise((res) => {
      setTimeout(() => {
        console.log('resolved');
        this.setState({ state: 5 });
        res();
      }, 5000);
    });
  }
}

const PromiseTest = lazy(async () => {
  return {
    default: TestInner,
  };
});

const TestApp: FC = () => {
  return (
    <div className='board-list-container'>
      <Suspense fallback={<div>WAITING...</div>}>
        <PromiseTest />
      </Suspense>
    </div>
  );
}

こうすると、動きました。動いてしまいました。え、なんでなんや、、、:thinking:
……Function Componentが使えない極めてまれなケースの1つを発見した身としては、非常に頭が痛いです。こういう重要なことはもっとわかりやすくドキュメントに書いておいて下せぇ……。

結論

React、なんもわからん。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[備忘録]Next.jsプロジェクト導入方法

個人の備忘録です。

Next.jsの導入方法

前提条件

Node.jsは導入済みとする。

方法

プロジェクトフォルダにpackage.jsonファイルを作成する。
内容は以下の通り

"package.json"
{
   "scripts": {
      "dev": "next",
      "build": "next build",
      "start": "next start",
      "export": "next export"
  }
}

フォルダ内で以下のコマンドを実行する。

npm install --save next react react-dom

ページ作成

プロジェクトフォルダにpagesフォルダを作成する.
このフォルダがWebページを配置しておくための場所になる。

このフォルダ内にindex.jsファイルを作成する。
内容は下記の通り

index.js
export default () =>
  <div>
    <h1>Next.js</h1>
    <p>新しいプロジェクトです!</p>
  </div>

プロジェクトの実行

プロジェクトフォルダ内で下記のコマンドを実行

npm run dev

ブラウザで確認する。
スクリーンショット 2019-12-24 20.13.23.png

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

作者直伝! Fluxフレームワーク Fleur 完全攻略ガイド

Frame(1).png

こんにちは〜 pixiv(VRoid Hub)のフロントエンドエンジニアでFleur開発者のHanakla(Twitter: @_ragg_) です!

React #2 Advent Calendar 2019 24日目となるこの記事では、モダンで小中規模くらいのフロントエンドのためのReact用Fluxライブラリ「@fleur/fleur」について作者としてダイマさせて頂きます!

Fleurについては、今年5月にpixiv insideにてご紹介させていただきましたが、この時よりさらに改善されていますので、その点も含めて解説していきたいと思います!

@ky7ieeeさんのTypescriptとReact HooksでReduxはもうしんどくないという記事が出ているところ恐縮なんですが、typescript-fsa… お前3年前に居てほしかったよ……!)

話すこと

  • VRoid Hubの実際の構成を基にしたFleurでのアプリケーション設計
  • Redux設計パターン・Redux Style Guideとのかみ合わせ
    (この規則はいいぞ、この規則は現実的じゃないぞなど)

こういう時にFleurを使ってくれ

  • とりあえず何も考えず最速で堅牢なそこそこスケールするSPAを組みたい!
    • 「君が欲しい物 - Best Practice Remix -」がFleurにはあります。
    • redux-thunk or redux-saga、reselect、typescript-fsa、Fleurには全部ある!
    • Next.jsでも使える!!!
  • Bless of Type(型の祝福)を気持ちよく受けながらSPAを作りたい
    • コードの書き心地はReduxに頑張って型をつけるよりめちゃくちゃにいいです。これは自信を持って言えます。FleurはTypeScriptの型推論を受けるための本当に最小限のAPIを用意しています。
  • ちゃんとテストしたい!
    • FleurはOperations / Store / Componentのそれぞれに対してテストのし易いAPIを提供しています。
      • 後述しますが、外部通信処理のDIも非常にシンプルに実装されています。
    • ここらへんは@fleur/testingというライブラリにまとまっています。
  • やんごとなき事情でStoreにJSONじゃないオブジェクトとか副作用がど〜〜〜しても必要!
    • 基本的にはJSONを使えというのはReduxと同じ方針だけど、リアルワールドにおいてはThree.jsのインスタンスや、レンダリングエンジンのインスタンスを状態管理に載せなくちゃならない場面があるんだ

世の中のSPAの8割くらいのケースを満たせる割と薄いFluxライブラリがここにある。少なくとも動画編集ソフトくらいまでなら動かせている。

さあ、いますぐyarn add @fleur/fleur @fleur/react

Fleur - A fully-typed, type inference and testing friendly Flux framework

Fleurは2019年的なAPIで、書きやすく、より型に優しく、テスタブルに構成されたライブラリです。
FluxibleReduxをベースにしていますが、基本的な設計に対して迷いや再実装が生じづらいようにAPIを設計しています。

TypeScriptがある時代前提で設計されているので、Reduxで型にお祈りを捧げるときにありがちなexport enum ほへActionTypeとかexport type なんとかActionTypes = ReturnType<typeof ほげ> みたいなのを頑張って書く必要がありません。これが標準で提供されているの圧倒的にアドです。Redux初心者がtypescript-fsaにたどり着くにはあまりにも超えなければいけない障壁が多すぎます。

またNext.jsとの併用にも対応しており、npx create-fleur-next-appコマンドでNext.js + FleurによるSPA開発も可能です。(Next.jsなしのSSRでもご利用いただけます、ルーター用意されてます。

Redux devtoolsにもとりあえず対応しているため、デバッグツールにも困らないと思います。

使い方・設計編

それではFleurの使い方、基本的な設計パターンを見ていきましょう。ここでは「VRoid Hub」をFluerで実装するというケースでコードを例示していきます。
Fleurは大まかに↓の4つの要素を必要としています。

Frame-1-1024x634.png

オススメのディレクトリ構成

Fleurでは、Re-ducks パターン風のディレクトリ構成を推奨しています。Re-ducksパターンは弊社の色々なプロダクトでも割とよく採用されている構成です。

Fleurが推奨する構成は具体的には以下のようになります。

- app
  - spec -- ここにテスト用のモック関数とかを詰める
    - mocks.ts
  - components
  - domains
    - Character
      - operations.ts
      - actions.ts
      - selectors.ts
      - store.ts
      - model.ts -- フロント側でどうしても必要なビジネスロジックは関数化してここに置く
      - index.ts -- Re-ducksパターンではあるけど任意。暇なら置いてよい
      - operations.test.ts
      - selectors.test.ts
      - store.test.ts
      - model.test.ts
    - User
      - operations.ts
      - actions.ts
      - selectors.ts
      - store.ts
      - model.ts

Re-ducksパターンでドメインを分けていくと、selectorやmodelがそのドメインに関係していないといけないことを強要出来るので、「汎用的なutils.tsにドメインロジックをアーーーーー????」みたいな事態を防げます。

もっともこの構成は、あくまで中規模化したプロダクトに対しての推奨で、より小さなプロダクトやプロトタイピングには手間が多いと思います。最小構成で行くならdomains/{Character,User}.tsにActionとかOperationsとかをドメイン毎に全部詰めるみたいな構成でもいいでしょう。exportがごちゃごちゃしてなければ最低限の治安は保てるはずです。

大規模アプリでどういう構成にしたらいいのかというのは今調べています。みなさんのプロダクトのディレクトリ構成とかフロントエンドアーキテクチャを語る記事を募集しています。

それでは各ファイルの中身を見ていきましょう

Operations

まずはOperation(アプリにおける手続き)を定義します。とりあえずキャラクター情報を取得してみましょうか。キャラクターにはキャラクターの情報(Character entity)とその投稿ユーザーの情報(User entity)があるものとします。

domains/Character/operations
import { operations } from '@fleur/fleur'
import { CharacterActions } from 'domains/Character/actions'
import { UserActions } from 'domains/User/actions'
import { normalize } from 'domains/normalize'
import { AppSelectors } from 'domains/App/selectors'
import { API } from 'domains/api'

export const CharacterOps = operations({
  // 特定のキャラクターの情報を取得する
  async fetchCharacter(context, characterId: string) {
    context.dispatch(CharacterActions.fetching.started, { characterId })

    // 認証情報取る
    const credential = AppSelectors.getCredential(context.getStore)

    try {
      // APIからデータを取る
      const response = await context.depend(API.getCharacter)(credential, characterId)

      // Entityを正規化したりDateに変換したりは`normalize`でやったことにする
      const { user, character } = normalize(response)

      // 正規化したデータをStoreに送りつける
      context.dispatch(CharacterActions.charactersFetched, [ character ])
      context.dispatch(UserActions.usersFetched, [ user ])
      context.dispatch(CharacterActions.fetching.done, { characterId })
    } catch (error) {
      rethrowIfNotResponseError(error)
      context.dispatch(CharacterActions.fetching.failed, { characterId, error })
    }
  },
  // 他のoperationの定義が続く
})

Operationで使うAPIと設計のコツを見てみましょう

  • context.getStore - Storeのインスタンスを取れます。
    context.getStore(CharacterStore)のような感じで使いますが、selectorに任せてしまうので直接コールする機会はあんまりないかもしれません。
  • context.depend - 渡されたオブジェクトをそのまま返します。「は?」って感じですね。
    これはテスト時にDependency Injectionを行うための仕組みです。後述します。
  • normalize - エンティティの正規化はOperationでやりましょう。純粋関数として切り出しておくとテストもしやすくて良いです。少なくともStoreで正規化するのはDRYじゃないのであまりおすすめしません…
  • context.dispatch - Actionを発行します。

normalizeの正規化単位

APIから振ってきたJSONは基本的にはEntity単位で切っていきます。
例えばVRoid HubのCharacter Entityは以下のような構造でAPIから降ってきます。

interface SerializedCharacter {
  character_id: string
  name: string
  create_at: string
  /** 投稿者 */
  user: {
    user_id: string
    name: string
    icon: SerializedUserIcon
  }
}

このJSONをDB的に分割するとCharacterUserUserIconになります。
しかし、UserとUserIconは基本的にセットで使われているので、特に分割する必要がありません。なので分割せず、以下のような2つのEntityに正規化しています。

interface Character {
  character_id: string
  name: string
  created_at: Date
  user_id: string
}

interface User {
  user_id: string
  name: string
  icon: SerializedUserIcon
}

Actions

次にActionsの定義です。Fleurにおいてこれはただの識別子と型宣言であり、Reduxと違ってこのActions自体はコールすることは出来ません。アプリケーションでどういうイベントが起きるかを宣言しているのみです。

domains/Character/actions.ts
import { actions, action } from '@fleur/fleur'
import { CharacterEntity } from "./types";

export const CharacterActions = actions(/* Redux Devtools用の識別子 = */ 'Characters', {
  // action名: action<ペイロードの型>()
  charactersFetched: action<CharacterEntity[]>(),
  fetching: action.async<
    { characterId: string }, 
    { characterId: string },
    { characterId: string, error: Error }
  >(),
})

fetchingcharactersFetchedが並んでるのがモニョっとしますね。しかしCharacter Entityが降ってくるのはキャラクターをフェッチしたときだけとは限らないので、あくまでフェッチ状況を伝えるActionと、実際にフェッチされたEntityを伝えるActionを分けています。

他のEntityを正規化した時にCharacter Entityが取り出されて、他のドメインからcharactersFetchedが起きたときにCharacterActions.fetching.doneするのが適切か?通信状態も一緒にごまかさないといけなくて設計がちょっと大変じゃない?という感じですね。


Action名は過去形を使うようにしましょう。Redux Style guide@f_subalさんのスライド でも言及されていますが、Actionを受け取った側がどういう処理をすべきなのかが伝わりやすくなります。

ただ一点、Redux Style Guideで述べられている「一つのActionで全ての関係Reducerが反応するようにすべき」という点には一概に賛同していません。

実はそのような構造にすると、特に大規模なアプリケーションにおいて「あるActionによってアプリケーションで何が発生するのか」が人間的に予測しづらくなり、実は適度な粒度でActionを連投した方が処理の流れが自明になることがあります。(VRoid Hubではエンティティ種毎にusersFetched, charactersFetchedのようにactionを連投する形にしています。)

特にFleurでどうしても仕方なくStore側で副作用を持っている場合は、どういう副作用を起こすかによってActionを切り分けた方がよさそうです。

またそれが推奨されている理由のもう一つに、ReduxではActionの連投はパフォーマンスに良くないというものがあるそうですが、FleurではStoreからの変更通知はrequestAnimationFrameでバッファリングされているため、あまり気にしなくてよいです。

Store

続いてStoreです。 Fleurにはclass-style StoreとreducerStoreがありますが、基本的にreducerStoreの利用を推奨しています。こちらは副作用を持てないStoreなので、どうしてもStoreで副作用が必要なときはclass-style Storeを利用します。(class-style Storeの書き方はこちらをご参照ください。)

domains/Characters/store.ts
import { reducerStore } from '@fleur/fleur'
import { CharacterActions } from './actions'
import { CharacterEntity } from '../CharacterEntity/types'
interface State {
  characters: { [characterId: string]: CharacterEntity | void }
  fetching: { [characterId: string]: { fetching: boolean, error?: Error } }
}

export const CharacterStore = reducerStore<State>('Character', () => ({ 
  characters: {},
  fetching: {},
}))
  .listen(CharacterActions.charactersFetched, (state, characters) => {
    characters.forEach(c => state.characters[c.id] = c)
  })
  .listen(CharacterActions.fetching.started, (state, { characterId }) => {
    state.fetching[characterId] = { fetching: true }
  })
  .listen(CharacterActions.fetching.done, (state, { characterId }) => {
    state.fetching[characterId] = { fetching: false }
  })
  .listen(CharacterActions.fetching.error, (state, { characterId, error }) => {
    state.fetching[characterId] = { fetching: false, error }
  })
  • reducerStore<State>(storeName, initialStateFactory)でStoreを宣言します。
    • storeNameはアプリ内で一意の名前である必要があります。SSR時に吐き出すJSONの名前空間の識別に利用されますが、SSRなしの場合でも必須です。
    • initialStateFactoryはStoreの初期状態を返す関数を渡します。
  • ReducerStore.listen(action, callback) でactionに対する処理を指定します。
    • state を直接変更していますが、これはimmerでラップされたdraftオブジェクトなので、実際のstateはイミュータブルに変更されます。
      • これは極めて強くて、特にReact Hooksとの組み合わせにおけるメモ化ではめちゃくちゃ楽にメモ化条件を設定することが出来ます。
  • Store内で外部通信などの副作用を起こさないようにしてください。副作用はできるだけOperation層に集めてください。

Component

ではここまで書いてきたものをコンポーネントに繋いでいきます。

pages/character.tsx
import React, { useCallback, useState, ChangeEvent } from 'react'
import { CharacterSelectors } from 'domains/Characters/selectors'
import { useStore, useFleurContext } from '@fleur/react'
import { UserSelectors } from 'domains/Users/selectors'
import { CharacterOps } from 'domains/Characters/operations'
import { API } from 'domains/api'

export const CharacterPage = () => {
  // URLからテキトーにキャラクターIDを取ってくる
  const characterId = 1

  const { executeOperation, depend } = useFleurContext()
  const character = useStore(getStore =>
    CharacterSelectors.getById(getStore, '1')
  )
  const user = useStore(getStore =>
    character ? UserSelectors.getById(getStore, character.user_id) : null
  )

  const handleChangeName = useCallback(
    ({ currentTarget }: ChangeEvent<HTMLInputElement>) => {
      if (!character) return
      depend(API.putCharacter)({ name: currentTarget!.value })
      executeOperation(CharacterOps.fetchCharacter, character.id)
    },
    [character]
  )

  if (!character || !user) {
    return <div>{/* いい感じのスケルトンを出す */}</div>
  }

  return (
    <div>
      <h1>
        <input
          type="text"
          defaultValue={character.name}
          onChange={handleChangeName}
          data-testid="input"
        />
      </h1>
      <h2>
        <a href={`/users/${user.id}`} data-testid="author">
          {user.name}
        </a>
      </h2>
    </div>
  )
}

ここで出てきたAPIを解説します。

  • useFleurContext - Operationを実行するためのexecuteOperation(), DIのためのdepend(), Storeの値の遅延取得のためのgetStore()が入ったオブジェクトを返します。
    • executeOperation(operation, ...args) - 第一引数に渡されたOperationを実行します。
    • depend(obj) - objを取得します。テスト時にobjをモックに差し替えることが出来ます。
    • getStore(Store) - Storeのインスタンスを取得します。基本的にuseStoreで値をとってくるので余り使うことはないと思いますが、表示には関係ないけどStoreから値を取らないといけない場合に使います。

コンポーネント内の属性に出てくるdata-testidは、後述するテストで利用します。

Selector

Selectorはこんな感じに用意してあげます。

domains/Characters/selectors.ts
import { selector } from "@fleur/fleur";
import { CharacterStore } from "./store";

export const CharacterSelectors = {
  getById: selector(
    (getState, id: string) => getState(CharacterStore).characters[id]
  )
}
  • Component側のuseStoreではgetStoreでしたが、selector内ではgetStateです
    • Store#stateを取得してくるためです。
    • Storeのインスタンス自体にアクセスする必要がある場合、selector()の代わりにselectorWithStore()を使います。

Bootstrap

最後にアプリの立ち上げ部分を書きます

app.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import Fleur from '@fleur/fleur'
import { CharacterStore } from 'domains/Characters/store'
import { UserStore } from 'domains/Users/store'
import { FleurContext } from '@fleur/react'

const app = new Fleur({
  stores: [CharacterStore, UserStore]
})

const context = app.createContext()

window.addEventListener('DOMContentLoaded', () => {
  const root = document.getElementById('#root')

  ReactDOM.render(
    <FleurContext value={context}>
      <App />
    </FleurContext>,
    root
  )
})
  • new FleurでFleurインスタンスを作ります
    • stores オプションにアプリ内で利用しているStoreを全て渡します。

Fleurでのテスト

ここからは今まで書いてきたOperation, Action, Componentに対してのテストを書いていきます。
Fleurのテストには@fleur/testingというパッケージを利用します。

yarn add -D @fleur/testing

テストフレームワークにはjest (with ts-jest)を利用する想定をしています。

Contextのモック

まずテストのためのモックContextを作ります。

spec/mock.ts
import { mockFleurContext, mockStore } from "@fleur/testing"
import { CharacterStore } from "domains/Characters/store"

const baseContext = mockFleurContext({
    stores: [
      // ここにアプリで使われるStoreをこの形式で突っ込む
      mockStore(CharacterStore)
    ]
  });

export const baseOperationContext = baseContext.mockOperationContext()
export const baseComponentContext = baseContext.mockComponentContext()

OperationとStoreのテストで利用するbaseOperationContext 、Componentのテストで利用するbaseComponentContextをexportしておきます。

Operationのテスト

はい、ではまずOperationのテストをしていきましょう。

domains/Character/operation.test.ts
import { CharacterOps } from './operations'
import { CharacterActions } from './actions'
import { UserActions } from 'domains/Users/actions'
import { API } from 'domains/api'
import { baseOperationContext } from 'spec/mock'
import { fakeRawCharacter } from 'spec/fakes/character'

describe('CharacterOps', () => {
  it('キャラクターとユーザーのEntityちゃんと投げた?', async () => {
    const context = baseOperationContext.derive(({ injectDep }) => {
      // Storeの特定の状態を設定する場合は `deriveStore` をする
      // deriveStore(AppStore, { credentialKey: 'mock' })

      // API.getCharacterをモックする
      injectDep(API.getCharacter, async (_, characterId) => fakeRawCharacter())
    })

    await context.executeOperation(CharacterOps.fetchCharacter, '1011')

    expect(context.dispatches[1]).toMatchObject({
      action: CharacterActions.charactersFetched
    })
    // expect(context.dispatches[1].payload).toMatchInlineSnapshot()

    expect(context.dispatches[2]).toMatchObject({
      action: UserActions.usersFetched
    })
    // expect(context.dispatches[2].payload).toMatchInlineSnapshot()
  })
})
  • injectDeps(元のオブジェクト, モックオブジェクト) によって、Operation内で.depend(...)しているオブジェクト(関数)をモックすることが出来ます
  • action.actionが関数なので一発でtoMatchInlineSnapshotしちゃうとちょっと信頼性に欠けます

context.dispatchesに発火されたActionの配列が入っているので、そのpayloadが意図した形になっているかどうかをチェックしていけば

Storeのテスト

Storeもテストしていきましょう

domains/Characters/store.test.ts
import { CharacterStore } from './store'
import { CharacterActions } from './actions'
import { baseOperationContext } from 'spec/mock'
import { fakeCharacter } from 'spec/fakes/character'

describe('CharacterStore', () => {
  it('エンティティがちゃんと保存されるか', () => {
    const context = baseOperationContext.derive(({ deriveStore }) => {
      deriveStore(CharacterStore, state => {
        state.characters['10'] = fakeCharacter({ id: '10' })
      })
    })
    const character = fakeCharacter()
    context.dispatch(CharacterActions.charactersFetched, [character])

    expect(
      context.getStore(CharacterStore).state.characters[character.id]
    ).toEqual(character)
  })
})

OperationContextからActionを投げて、意図したとおりのstateになっているかを検証します。
ここでは雑に1ケースしか書いてないですが、必要であればこの形式で書き足していきましょう。

その際、テストケース毎にbaseOperationContext.derive()で複製したcontextを使うことを推奨しています。
deriveは#operationのテストで書いたように、Storeの状態を派生させる事ができます。前提状態があるテストを書く場合に利用してください。

const context = baseOperationContext.derive(({ injectDep }) => {
  // オブジェクトはshallow-mergeされる
  deriveStore(CharacterStore, {
    characters: {
      '10': fakeCharacter();
    }
  })

  // Deep-mergeしたい時はコールバックを使う
  deriveStore(CharacterStore, (state) => {
    state.characters['10'] = fakeCharacter()
  })
});

Componentのテスト

最後にComponentのテストです。 コンポーネントのレンダリングには@testing-library/reactを利用します。

import { render, getByTestId, fireEvent } from '@testing-library/react'
import { TestingFleurContext } from '@fleur/testing'
import React from 'react'
import { baseComponentContext } from 'spec/mock'
// ... 略

describe('Character', () => {
  const mockedContext = baseComponentContext.derive(({ deriveStore }) => {
    deriveStore(CharacterStore, state => {
      state.characters['1'] = fakeCharacter({
        id: '1',
        name: 'Haru Sakura',
        user_id: '2'
      })
    })

    deriveStore(UserStore, state => {
      state.users['2'] = fakeUser({
        id: '2',
        name: 'Hanakla'
      })
    })
  })

  // Case.1 キャラクターの情報ちゃんと出てる?
  // Case.2 APIリクエストちゃんと飛ぶ?
})

baseComponentContext.deriveによりStoreの状態を設定したContextを生成します。
これを次のテストケースに食わせてあげると、特定の状態下で正しくコンポーネントが動くかをテストできます。

Case1
  it('キャラクターの情報ちゃんと出てる?', async () => {
    const context = mockedContext.derive()

    const tree = render(
      <TestingFleurContext value={context}>
        {/* <なんとかRouter url='/characters/1'> */}
        <CharacterPage />
        {/* </なんとかRouter> */}
      </TestingFleurContext>
    )

    expect(tree.getByTestId('input').value).toBe('Haru Sakura')
    expect(tree.getByTestId('author').getAttribute('href')).toBe('/users/2')
    expect(tree.getByTestId('author').innerHTML).toBe('Hanakla')
  })

mockedContext.derive()でテストケース用のContextを初期化し、TestingFleurContext Componentに与えます。これにより、内部のFleur Contextをモックすることが出来ます。

あとは[data-testid]を元に要素を探して適切な値が出てるかを調べてるだけですね。
TestingFleurContext で囲うということだけ覚えておいてください。

次にAPIリクエストのテストをします。

Case2
  it('APIリクエストちゃんと飛ぶ?', async () => {
    const apiSpy = jest.fn(async () => void 0)
    const context = mockedContext.derive(({ injectDep }) => {
      injectDep(API.putCharacter, apiSpy)
    })

    const tree = render(
      <TestingFleurContext value={context}>
        <なんとかRouter url='/characters/1'>
        <CharacterPage />
        </なんとかRouter>
      </TestingFleurContext>
    )

    fireEvent.change(tree.getByTestId('input'), {
      target: { value: 'Haru' }
    })

    // Wait for request
    await new Promise(r => setTimeout(r))

    expect(apiSpy).toBeCalledWith({ name: 'Haru' })
    expect(context.executes[0]).toMatchObject({
      op: CharacterOps.fetchCharacter,
      args: ['1']
    })
  })
  • Operationのテストでも登場したinjectDepがここでも登場します。 Componentから外部通信を起こすようなパターンの設計をしている場合、ここでdepend / injectDepを使うことで関数をモックすることが出来ます。
  • Componentから発火されたexecuteOperationの内容は`context.

裏話

Fleurの生まれ

FleurはDelirというWeb製映像編集ソフトを開発していく過程で生まれました。

当初はFlux Utilsを使って構築されていましたが、直接的なStoreへの依存や、Action CreatorからStoreを触れないのはComponentが表示以上の責務を負わなければならないなどの問題があり、より適切な移行先を探す中でFluxibleにたどり着きました。Reduxはかなり要件のアンマッチがあり採用しませんでした。

  • Storeに副作用が持てない
    • JSONを扱うのが基本
    • じゃあレンダリングエンジンのインスタンスはどこでもつのか? Component側か?アプリケーション全体のあらゆるイベントを受け付けないといけないのにComponent? React Context使ってインスタンス配っても状態変化の予測がつかない狂気になるだけじゃん?
      • Storeに突っ込むのが保守性・予測性が良い。ならReduxは使えない…
      • レンダリングエンジンが自身で副作用を発してもComponent側からはただのStoreの変化として扱える、合理的。
  • 標準で非同期処理に対応していない
  • 型付けのためだけのコードが多くてしんどい
    • ActionNamesとActionの型定義が離れてるの果てしなくやりづらい

しかしFluxibleも2016年くらいのときには既につらみを抱えていました。TypeScriptの台頭です。
Fluxibleは上記の要件的には完全に一致しており、コードの読み書きにもほとんど困らないAPIでしたが、こと型をつけるという文脈ではかなり無理のあるAPIになっていました。(pixivFACTORYに所属していた時にFluxibleを採用し型定義を自前で行いましたが、相当しんどいものでした。)

そこでFluxibleを元に、より現代的な設計パターンやコードの書きやすいAPIを取り入れたFluxフレームワークを作る、という目的でFleurが誕生しました。

  • 型推論フレンドリー
  • コードの書き心地を良くする
  • React Hooks対応
  • 非同期処理対応 (Fluxibleは対応済み)
  • immer.js組み込みのStore
    • 大体のケースで使ったほうが良い。 まだImmutable.jsが良いとかImmer.jsがES5で使えない思ってる人いないよね?
  • パフォーマンス上のボトルネックにならない
    • Reactの再レンダリングによって映像レンダリングのFPSが落ちる。UXに如実に影響を与えるのでこれは必要な要件だった。

さらに、本業で得たSSR周りや一般的なWebアプリケーションに対する設計に関する知識を取り込み、通常のWebアプリケーションやSSR環境でも利用可能なフレームワークとしてキメキメにしていきました。

そういう経緯があり、Fleurは2019年までのSPA用ライブラリとしてはn番煎じをキメ込んで非常にまとまりが良く保守性・汎用性の高いフレームワークに仕上がりました。

SSR対応

SSR要件もちゃんと気にかけており、SSRを行うデモアプリを運用して、Apache Benchで現実的じゃない量のアクセスをかけてメモリリークが起きていない(GCでちゃんとメモリが開放されること)を確認したりしてします。

今回のサンプルにも載せられなかったんですが、ルーターも用意されています。

最後に

言い訳させてください! 完全攻略ガイドと言っておきながら体調不良によりNext.jsとかSSRとかに結局触れられませんでした!そこらへんは後日別記事でやるかもしれません(この記事のウケがよかったらね)

本記事で書いたコードはra-gg/advent-calendar-fleurにまとまっています。(実は雰囲気コードなので全部のテストは通ってないんだけどね)

とりあえず自分の中では2019年までのSPA設計のまとめとしてはいい感じのフレームワークになっているかなと思います。残念ながら社内プロダクトでの採用の機会はまだないんですけどね〜! 来年は2020年なのできっとさらに色々良くなったFleurをお見せできるかと思います!

もしよろしければ、「雑にFleur使ってみた」「作者が怠惰だからオレがFleur完全攻略してみた」など、Fleurに対する感想、不満、オレの設計語りなどインターネットに放流してください! Fleur開発に対するモチベーションと設計の洗練度が上がるので! Twitterハッシュタグは #fleurjs GitHub Topic / Qiita tagは fleur-js です!

Q & A

2019年的?

2019年的です。2020年的ではないという意味でもあります。色々情報を集めてはいますがまだよくなれる余地がありそうな気がしています。マイグレーションしやすいことは考えていますが、Breaking changeは入れていくと思います。

Next.jsで使える?

使えます。いますぐyarn create fleur-next-app <your-app-name>しましょう。
本記事で解説したのとほぼ同等の構成のファイル郡でアプリ開発をおっ始める事ができます。

そのうちNext.js + Fleurでアプリを作る記事でも書こうかと思います。

画面毎にStoreを分けるのどう思う?

いいと思う。ただその場合この記事で解説したdomain毎の分割だけだと治安が悪くなるので、page毎にStoreを入れるディレクトリを上手く分ける必要はありそう。ただし現バージョンのFleurはあまりそれがやりやすい構造になってないので、それについては今後の課題かなと思っています。

useReducerを薄〜くラップしたライブラリがあるとそういう「アプリ全体には影響ないけど、ある特定のページ以下でめっちゃグローバル」みたいなやつを切り出せるんだよな〜とも思っています。これも考えてる。既にいくつかそれらしいライブラリがあった気もしている。

なんでOperationとActionが別れてるの?

ActionとOperationが1ファイルに混じってごちゃごちゃするのが嫌だったのと、Re-ducksパターンによる影響と、実際ActionとOperationってそんなに綺麗に一対一になるか?という気持ちに決着がつけられていないためです。typescript-fsaはその点すごくシンプルですね。ActionとOperationが透過的に扱えるRedux故の特徴です。 Fleurも何かしら考えたほうがいいよな〜とは思っています。

middlewareない?

ない。middlewareを入れると実装の見通しが悪くなるので、簡単に入れられる仕組みを入れたくない。どうしても必要だったらcontextをラップしてくれ。(Redux DevTools対応はその方式でやってる)

const yourMiddleware = (context: AppContext) => {
  const executeOperation = context.executeOperation
  const dispatch = context.dispatch

  context.executeOperation = (op, ...args) => {
    // なにがしの処理をする
    context.dispatch(MiddlewareActions.hoge, {})
    return executeOperation(op, ...args)
  }

  context.dispatch = (action, payload) => {
    if (action.name.indexOf('.started')) {
      // ローディングを出したりする
    }

    return dispatch(action, payload)
  }

  return context
}

const app = new Fleur();
const context = yourMiddleware(app.createContext())

Fleurのselectorはmemoizeしてる?

してない。 関数一発でmemoizeしてもキャッシュヒット率がたかが知れてるのでしてない。

代替案としてはReactのuseMemoを使う形に寄せて欲しい。useMemoならコンポーネントの文脈に沿ったよりヒット率の高いmemoizeができる。

reselectのコードを読んだことがあればわかると思うけど(ここらへんな)、あるselectorが複数回異なる引数で呼ばれるような場合、reselectのmemoizeはまともに機能しないっぽく見える。ある引数でmemoizeしても次に異なる引数でselectorが呼ばれればキャッシュは破棄される。そして真面目にプロダクトコード書いてたらこれは普通に発生する。

EcmaScriptのRicher keys - compositeKeyが入ればWeakMapで今よりはマシなmemoize機構を作れそうだけど、FleurもReduxも別々の問題でキャッシュヒット率には難がありそうな予感がしています。

なんでComponentからdispatch出来ないの?

dispatchはStoreに対するかなり低レイヤーな操作です。もしこれをComponentから使えるようにしてしまうと、アプリの構造化が属人的になりやすくなってしまいます。(どこからどういう粒度でdispatchするかを考える余地が生まれてしまう)

とはいえ昨今のイケてるフロントエンドを見ているとReact SuspenseとかuseAsyncなどを使ってComponent側から通信を起こすケースがありがちなので、そろそろ許しても良いかもしれない…けどアプリが大きくなってもそれを続けられるのか…?小規模アプリだから許されてるのでは…? そもそもその手のアプリはFluxライブラリ使ってないんじゃ…? というところで悩んでいます。

俺はVue派なんだが?

Vue版も作ろうか悩んでる、やればVuexより型は強くなる。ただ内部事情としてimmer.jsとVueは相性が悪いとかいう次元じゃないのでFleurの書き直しかMutableStoreの対応が必要になる。
あとMobxとかVuexとか色々見ていて「Fleurは本当にこのままの設計でいいのかな〜?」という気持ちもあるのでそこらへん全てに踏ん切りがついたらやるかもね

(たぶんVueの人たちVue公式以外のもの使いたがらなさそうという気がするので暇すぎてひまわりになったら?やる)

パフォーマンスどんなもん?

ちょっと極端なケースで計測しているのですがFleur vs Reduxはこんな感じです。(masterの最新コミットでの比較)

image.png

Fluxibleも計測してるんですがあまりにも遅いしwarnが多いので省きました。

なんで君のサンプルコードexport defaultしてないの?

  • Named exportしておくとVSCodeのimport候補に出やすくなるから
  • 後から「あっこの名前よくなかったわ」って時にVSCodeの自動リファクタリングで一発で名前を変えたいから
    (default importされたやつは名前変更で一発で変わってくれない)
  • default importした時に、人によってimport物にどういう規則で名前つけるか揺れてほしくないから
    • import名に対してコーディング規約を考えるのしんどいから最初から適切な名前ついていてホシイ ? ?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2019年時代のFluxフレームワーク “Fleur” 完全攻略ガイド【作者直伝】

Frame(1).png

こんにちは〜 pixiv(VRoid Hub)のフロントエンドエンジニアでFleur開発者のHanakla(Twitter: @_ragg_) です!

React #2 Advent Calendar 2019 24日目となるこの記事では、モダンで小中規模くらいのフロントエンドのためのReact用Fluxライブラリ「@fleur/fleur」について作者としてダイマさせて頂きます!

Fleurについては、今年5月にpixiv insideにてご紹介させていただきましたが、この時よりさらに改善されていますので、その点も含めて解説していきたいと思います!

@ky7ieeeさんのTypescriptとReact HooksでReduxはもうしんどくないという記事が出ているところ恐縮なんですが、typescript-fsa… お前3年前に居てほしかったよ……!)

話すこと

  • VRoid Hubの実際の構成を基にしたFleurでのアプリケーション設計
  • Redux設計パターン・Redux Style Guideとのかみ合わせ
    (この規則はいいぞ、この規則は現実的じゃないぞなど)

こういう時にFleurを使ってくれ

  • とりあえず何も考えず最速で堅牢なそこそこスケールするSPAを組みたい!
    • 「君が欲しい物 - Best Practice Remix -」がFleurにはあります。
    • redux-thunk or redux-saga、reselect、typescript-fsa、Fleurには全部ある!
    • Next.jsでも使える!!!
  • Bless of Type(型の祝福)を気持ちよく受けながらSPAを作りたい
    • コードの書き心地はReduxに頑張って型をつけるよりめちゃくちゃにいいです。これは自信を持って言えます。FleurはTypeScriptの型推論を受けるための本当に最小限のAPIを用意しています。
  • ちゃんとテストしたい!
    • FleurはOperations / Store / Componentのそれぞれに対してテストのし易いAPIを提供しています。
      • 後述しますが、外部通信処理のDIも非常にシンプルに実装されています。
    • ここらへんは@fleur/testingというライブラリにまとまっています。
  • やんごとなき事情でStoreにJSONじゃないオブジェクトとか副作用がど〜〜〜しても必要!
    • 基本的にはJSONを使えというのはReduxと同じ方針だけど、リアルワールドにおいてはThree.jsのインスタンスや、レンダリングエンジンのインスタンスを状態管理に載せなくちゃならない場面があるんだ

世の中のSPAの8割くらいのケースを満たせる割と薄いFluxライブラリがここにある。少なくとも動画編集ソフトくらいまでなら動かせている。

さあ、いますぐyarn add @fleur/fleur @fleur/react

Fleur - A fully-typed, type inference and testing friendly Flux framework

Fleurは2019年的なAPIで、書きやすく、より型に優しく、テスタブルに構成されたライブラリです。
FluxibleReduxをベースにしていますが、基本的な設計に対して迷いや再実装が生じづらいようにAPIを設計しています。

TypeScriptがある時代前提で設計されているので、Reduxで型にお祈りを捧げるときにありがちなexport enum ほへActionTypeとかexport type なんとかActionTypes = ReturnType<typeof ほげ> みたいなのを頑張って書く必要がありません。これが標準で提供されているの圧倒的にアドです。Redux初心者がtypescript-fsaにたどり着くにはあまりにも超えなければいけない障壁が多すぎます。

またNext.jsとの併用にも対応しており、npx create-fleur-next-appコマンドでNext.js + FleurによるSPA開発も可能です。(Next.jsなしのSSRでもご利用いただけます、ルーター用意されてます。

Redux devtoolsにもとりあえず対応しているため、デバッグツールにも困らないと思います。

使い方・設計編

それではFleurの使い方、基本的な設計パターンを見ていきましょう。ここでは「VRoid Hub」をFluerで実装するというケースでコードを例示していきます。
Fleurは大まかに↓の4つの要素を必要としています。

Frame-1-1024x634.png

オススメのディレクトリ構成

Fleurでは、Re-ducks パターン風のディレクトリ構成を推奨しています。Re-ducksパターンは弊社の色々なプロダクトでも割とよく採用されている構成です。

Fleurが推奨する構成は具体的には以下のようになります。

- app
  - spec -- ここにテスト用のモック関数とかを詰める
    - mocks.ts
  - components
  - domains
    - Character
      - operations.ts
      - actions.ts
      - selectors.ts
      - store.ts
      - model.ts -- フロント側でどうしても必要なビジネスロジックは関数化してここに置く
      - index.ts -- Re-ducksパターンではあるけど任意。暇なら置いてよい
      - operations.test.ts
      - selectors.test.ts
      - store.test.ts
      - model.test.ts
    - User
      - operations.ts
      - actions.ts
      - selectors.ts
      - store.ts
      - model.ts

Re-ducksパターンでドメインを分けていくと、selectorやmodelがそのドメインに関係していないといけないことを強要出来るので、「汎用的なutils.tsにドメインロジックをアーーーーー????」みたいな事態を防げます。

もっともこの構成は、あくまで中規模化したプロダクトに対しての推奨で、より小さなプロダクトやプロトタイピングには手間が多いと思います。最小構成で行くならdomains/{Character,User}.tsにActionとかOperationsとかをドメイン毎に全部詰めるみたいな構成でもいいでしょう。exportがごちゃごちゃしてなければ最低限の治安は保てるはずです。

大規模アプリでどういう構成にしたらいいのかというのは今調べています。みなさんのプロダクトのディレクトリ構成とかフロントエンドアーキテクチャを語る記事を募集しています。

それでは各ファイルの中身を見ていきましょう

Operations

まずはOperation(アプリにおける手続き)を定義します。とりあえずキャラクター情報を取得してみましょうか。キャラクターにはキャラクターの情報(Character entity)とその投稿ユーザーの情報(User entity)があるものとします。

domains/Character/operations
import { operations } from '@fleur/fleur'
import { CharacterActions } from 'domains/Character/actions'
import { UserActions } from 'domains/User/actions'
import { normalize } from 'domains/normalize'
import { AppSelectors } from 'domains/App/selectors'
import { API } from 'domains/api'

export const CharacterOps = operations({
  // 特定のキャラクターの情報を取得する
  async fetchCharacter(context, characterId: string) {
    context.dispatch(CharacterActions.fetching.started, { characterId })

    // 認証情報取る
    const credential = AppSelectors.getCredential(context.getStore)

    try {
      // APIからデータを取る
      const response = await context.depend(API.getCharacter)(credential, characterId)

      // Entityを正規化したりDateに変換したりは`normalize`でやったことにする
      const { user, character } = normalize(response)

      // 正規化したデータをStoreに送りつける
      context.dispatch(CharacterActions.charactersFetched, [ character ])
      context.dispatch(UserActions.usersFetched, [ user ])
      context.dispatch(CharacterActions.fetching.done, { characterId })
    } catch (error) {
      rethrowIfNotResponseError(error)
      context.dispatch(CharacterActions.fetching.failed, { characterId, error })
    }
  },
  // 他のoperationの定義が続く
})

Operationで使うAPIと設計のコツを見てみましょう

  • context.getStore - Storeのインスタンスを取れます。
    context.getStore(CharacterStore)のような感じで使いますが、selectorに任せてしまうので直接コールする機会はあんまりないかもしれません。
  • context.depend - 渡されたオブジェクトをそのまま返します。「は?」って感じですね。
    これはテスト時にDependency Injectionを行うための仕組みです。後述します。
  • normalize - エンティティの正規化はOperationでやりましょう。純粋関数として切り出しておくとテストもしやすくて良いです。少なくともStoreで正規化するのはDRYじゃないのであまりおすすめしません…
  • context.dispatch - Actionを発行します。

normalizeの正規化単位

APIから振ってきたJSONは基本的にはEntity単位で切っていきます。
例えばVRoid HubのCharacter Entityは以下のような構造でAPIから降ってきます。

interface SerializedCharacter {
  character_id: string
  name: string
  create_at: string
  /** 投稿者 */
  user: {
    user_id: string
    name: string
    icon: SerializedUserIcon
  }
}

このJSONをDB的に分割するとCharacterUserUserIconになります。
しかし、UserとUserIconは基本的にセットで使われているので、特に分割する必要がありません。なので分割せず、以下のような2つのEntityに正規化しています。

interface Character {
  character_id: string
  name: string
  created_at: Date
  user_id: string
}

interface User {
  user_id: string
  name: string
  icon: SerializedUserIcon
}

Actions

次にActionsの定義です。Fleurにおいてこれはただの識別子と型宣言であり、Reduxと違ってこのActions自体はコールすることは出来ません。アプリケーションでどういうイベントが起きるかを宣言しているのみです。

domains/Character/actions.ts
import { actions, action } from '@fleur/fleur'
import { CharacterEntity } from "./types";

export const CharacterActions = actions(/* Redux Devtools用の識別子 = */ 'Characters', {
  // action名: action<ペイロードの型>()
  charactersFetched: action<CharacterEntity[]>(),
  fetching: action.async<
    { characterId: string }, 
    { characterId: string },
    { characterId: string, error: Error }
  >(),
})

fetchingcharactersFetchedが並んでるのがモニョっとしますね。しかしCharacter Entityが降ってくるのはキャラクターをフェッチしたときだけとは限らないので、あくまでフェッチ状況を伝えるActionと、実際にフェッチされたEntityを伝えるActionを分けています。

他のEntityを正規化した時にCharacter Entityが取り出されて、他のドメインからcharactersFetchedが起きたときにCharacterActions.fetching.doneするのが適切か?通信状態も一緒にごまかさないといけなくて設計がちょっと大変じゃない?という感じですね。


Action名は過去形を使うようにしましょう。Redux Style guide@f_subalさんのスライド でも言及されていますが、Actionを受け取った側がどういう処理をすべきなのかが伝わりやすくなります。

ただ一点、Redux Style Guideで述べられている「一つのActionで全ての関係Reducerが反応するようにすべき」という点には一概に賛同していません。

実はそのような構造にすると、特に大規模なアプリケーションにおいて「あるActionによってアプリケーションで何が発生するのか」が人間的に予測しづらくなり、実は適度な粒度でActionを連投した方が処理の流れが自明になることがあります。(VRoid Hubではエンティティ種毎にusersFetched, charactersFetchedのようにactionを連投する形にしています。)

特にFleurでどうしても仕方なくStore側で副作用を持っている場合は、どういう副作用を起こすかによってActionを切り分けた方がよさそうです。

またそれが推奨されている理由のもう一つに、ReduxではActionの連投はパフォーマンスに良くないというものがあるそうですが、FleurではStoreからの変更通知はrequestAnimationFrameでバッファリングされているため、あまり気にしなくてよいです。

Store

続いてStoreです。 Fleurにはclass-style StoreとreducerStoreがありますが、基本的にreducerStoreの利用を推奨しています。こちらは副作用を持てないStoreなので、どうしてもStoreで副作用が必要なときはclass-style Storeを利用します。(class-style Storeの書き方はこちらをご参照ください。)

domains/Characters/store.ts
import { reducerStore } from '@fleur/fleur'
import { CharacterActions } from './actions'
import { CharacterEntity } from '../CharacterEntity/types'
interface State {
  characters: { [characterId: string]: CharacterEntity | void }
  fetching: { [characterId: string]: { fetching: boolean, error?: Error } }
}

export const CharacterStore = reducerStore<State>('Character', () => ({ 
  characters: {},
  fetching: {},
}))
  .listen(CharacterActions.charactersFetched, (state, characters) => {
    characters.forEach(c => state.characters[c.id] = c)
  })
  .listen(CharacterActions.fetching.started, (state, { characterId }) => {
    state.fetching[characterId] = { fetching: true }
  })
  .listen(CharacterActions.fetching.done, (state, { characterId }) => {
    state.fetching[characterId] = { fetching: false }
  })
  .listen(CharacterActions.fetching.error, (state, { characterId, error }) => {
    state.fetching[characterId] = { fetching: false, error }
  })
  • reducerStore<State>(storeName, initialStateFactory)でStoreを宣言します。
    • storeNameはアプリ内で一意の名前である必要があります。SSR時に吐き出すJSONの名前空間の識別に利用されますが、SSRなしの場合でも必須です。
    • initialStateFactoryはStoreの初期状態を返す関数を渡します。
  • ReducerStore.listen(action, callback) でactionに対する処理を指定します。
    • state を直接変更していますが、これはimmerでラップされたdraftオブジェクトなので、実際のstateはイミュータブルに変更されます。
      • これは極めて強くて、特にReact Hooksとの組み合わせにおけるメモ化ではめちゃくちゃ楽にメモ化条件を設定することが出来ます。
  • Store内で外部通信などの副作用を起こさないようにしてください。副作用はできるだけOperation層に集めてください。

Component

ではここまで書いてきたものをコンポーネントに繋いでいきます。

pages/character.tsx
import React, { useCallback, useState, ChangeEvent } from 'react'
import { CharacterSelectors } from 'domains/Characters/selectors'
import { useStore, useFleurContext } from '@fleur/react'
import { UserSelectors } from 'domains/Users/selectors'
import { CharacterOps } from 'domains/Characters/operations'
import { API } from 'domains/api'

export const CharacterPage = () => {
  // URLからテキトーにキャラクターIDを取ってくる
  const characterId = 1

  const { executeOperation, depend } = useFleurContext()
  const character = useStore(getStore =>
    CharacterSelectors.getById(getStore, '1')
  )
  const user = useStore(getStore =>
    character ? UserSelectors.getById(getStore, character.user_id) : null
  )

  const handleChangeName = useCallback(
    ({ currentTarget }: ChangeEvent<HTMLInputElement>) => {
      if (!character) return
      depend(API.putCharacter)({ name: currentTarget!.value })
      executeOperation(CharacterOps.fetchCharacter, character.id)
    },
    [character]
  )

  if (!character || !user) {
    return <div>{/* いい感じのスケルトンを出す */}</div>
  }

  return (
    <div>
      <h1>
        <input
          type="text"
          defaultValue={character.name}
          onChange={handleChangeName}
          data-testid="input"
        />
      </h1>
      <h2>
        <a href={`/users/${user.id}`} data-testid="author">
          {user.name}
        </a>
      </h2>
    </div>
  )
}

ここで出てきたAPIを解説します。

  • useFleurContext - Operationを実行するためのexecuteOperation(), DIのためのdepend(), Storeの値の遅延取得のためのgetStore()が入ったオブジェクトを返します。
    • executeOperation(operation, ...args) - 第一引数に渡されたOperationを実行します。
    • depend(obj) - objを取得します。テスト時にobjをモックに差し替えることが出来ます。
    • getStore(Store) - Storeのインスタンスを取得します。基本的にuseStoreで値をとってくるので余り使うことはないと思いますが、表示には関係ないけどStoreから値を取らないといけない場合に使います。

コンポーネント内の属性に出てくるdata-testidは、後述するテストで利用します。

Selector

Selectorはこんな感じに用意してあげます。

domains/Characters/selectors.ts
import { selector } from "@fleur/fleur";
import { CharacterStore } from "./store";

export const CharacterSelectors = {
  getById: selector(
    (getState, id: string) => getState(CharacterStore).characters[id]
  )
}
  • Component側のuseStoreではgetStoreでしたが、selector内ではgetStateです
    • Store#stateを取得してくるためです。
    • Storeのインスタンス自体にアクセスする必要がある場合、selector()の代わりにselectorWithStore()を使います。

Bootstrap

最後にアプリの立ち上げ部分を書きます

app.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import Fleur from '@fleur/fleur'
import { CharacterStore } from 'domains/Characters/store'
import { UserStore } from 'domains/Users/store'
import { FleurContext } from '@fleur/react'

const app = new Fleur({
  stores: [CharacterStore, UserStore]
})

const context = app.createContext()

window.addEventListener('DOMContentLoaded', () => {
  const root = document.getElementById('#root')

  ReactDOM.render(
    <FleurContext value={context}>
      <App />
    </FleurContext>,
    root
  )
})
  • new FleurでFleurインスタンスを作ります
    • stores オプションにアプリ内で利用しているStoreを全て渡します。

Fleurでのテスト

ここからは今まで書いてきたOperation, Action, Componentに対してのテストを書いていきます。
Fleurのテストには@fleur/testingというパッケージを利用します。

yarn add -D @fleur/testing

テストフレームワークにはjest (with ts-jest)を利用する想定をしています。

Contextのモック

まずテストのためのモックContextを作ります。

spec/mock.ts
import { mockFleurContext, mockStore } from "@fleur/testing"
import { CharacterStore } from "domains/Characters/store"

const baseContext = mockFleurContext({
    stores: [
      // ここにアプリで使われるStoreをこの形式で突っ込む
      mockStore(CharacterStore)
    ]
  });

export const baseOperationContext = baseContext.mockOperationContext()
export const baseComponentContext = baseContext.mockComponentContext()

OperationとStoreのテストで利用するbaseOperationContext 、Componentのテストで利用するbaseComponentContextをexportしておきます。

Operationのテスト

はい、ではまずOperationのテストをしていきましょう。

domains/Character/operation.test.ts
import { CharacterOps } from './operations'
import { CharacterActions } from './actions'
import { UserActions } from 'domains/Users/actions'
import { API } from 'domains/api'
import { baseOperationContext } from 'spec/mock'
import { fakeRawCharacter } from 'spec/fakes/character'

describe('CharacterOps', () => {
  it('キャラクターとユーザーのEntityちゃんと投げた?', async () => {
    const context = baseOperationContext.derive(({ injectDep }) => {
      // Storeの特定の状態を設定する場合は `deriveStore` をする
      // deriveStore(AppStore, { credentialKey: 'mock' })

      // API.getCharacterをモックする
      injectDep(API.getCharacter, async (_, characterId) => fakeRawCharacter())
    })

    await context.executeOperation(CharacterOps.fetchCharacter, '1011')

    expect(context.dispatches[1]).toMatchObject({
      action: CharacterActions.charactersFetched
    })
    // expect(context.dispatches[1].payload).toMatchInlineSnapshot()

    expect(context.dispatches[2]).toMatchObject({
      action: UserActions.usersFetched
    })
    // expect(context.dispatches[2].payload).toMatchInlineSnapshot()
  })
})
  • injectDeps(元のオブジェクト, モックオブジェクト) によって、Operation内で.depend(...)しているオブジェクト(関数)をモックすることが出来ます
  • action.actionが関数なので一発でtoMatchInlineSnapshotしちゃうとちょっと信頼性に欠けます

context.dispatchesに発火されたActionの配列が入っているので、そのpayloadが意図した形になっているかどうかをチェックしていけば

Storeのテスト

Storeもテストしていきましょう

domains/Characters/store.test.ts
import { CharacterStore } from './store'
import { CharacterActions } from './actions'
import { baseOperationContext } from 'spec/mock'
import { fakeCharacter } from 'spec/fakes/character'

describe('CharacterStore', () => {
  it('エンティティがちゃんと保存されるか', () => {
    const context = baseOperationContext.derive(({ deriveStore }) => {
      deriveStore(CharacterStore, state => {
        state.characters['10'] = fakeCharacter({ id: '10' })
      })
    })
    const character = fakeCharacter()
    context.dispatch(CharacterActions.charactersFetched, [character])

    expect(
      context.getStore(CharacterStore).state.characters[character.id]
    ).toEqual(character)
  })
})

OperationContextからActionを投げて、意図したとおりのstateになっているかを検証します。
ここでは雑に1ケースしか書いてないですが、必要であればこの形式で書き足していきましょう。

その際、テストケース毎にbaseOperationContext.derive()で複製したcontextを使うことを推奨しています。
deriveは#operationのテストで書いたように、Storeの状態を派生させる事ができます。前提状態があるテストを書く場合に利用してください。

const context = baseOperationContext.derive(({ injectDep }) => {
  // オブジェクトはshallow-mergeされる
  deriveStore(CharacterStore, {
    characters: {
      '10': fakeCharacter();
    }
  })

  // Deep-mergeしたい時はコールバックを使う
  deriveStore(CharacterStore, (state) => {
    state.characters['10'] = fakeCharacter()
  })
});

Componentのテスト

最後にComponentのテストです。 コンポーネントのレンダリングには@testing-library/reactを利用します。

import { render, getByTestId, fireEvent } from '@testing-library/react'
import { TestingFleurContext } from '@fleur/testing'
import React from 'react'
import { baseComponentContext } from 'spec/mock'
// ... 略

describe('Character', () => {
  const mockedContext = baseComponentContext.derive(({ deriveStore }) => {
    deriveStore(CharacterStore, state => {
      state.characters['1'] = fakeCharacter({
        id: '1',
        name: 'Haru Sakura',
        user_id: '2'
      })
    })

    deriveStore(UserStore, state => {
      state.users['2'] = fakeUser({
        id: '2',
        name: 'Hanakla'
      })
    })
  })

  // Case.1 キャラクターの情報ちゃんと出てる?
  // Case.2 APIリクエストちゃんと飛ぶ?
})

baseComponentContext.deriveによりStoreの状態を設定したContextを生成します。
これを次のテストケースに食わせてあげると、特定の状態下で正しくコンポーネントが動くかをテストできます。

Case1
  it('キャラクターの情報ちゃんと出てる?', async () => {
    const context = mockedContext.derive()

    const tree = render(
      <TestingFleurContext value={context}>
        {/* <なんとかRouter url='/characters/1'> */}
        <CharacterPage />
        {/* </なんとかRouter> */}
      </TestingFleurContext>
    )

    expect(tree.getByTestId('input').value).toBe('Haru Sakura')
    expect(tree.getByTestId('author').getAttribute('href')).toBe('/users/2')
    expect(tree.getByTestId('author').innerHTML).toBe('Hanakla')
  })

mockedContext.derive()でテストケース用のContextを初期化し、TestingFleurContext Componentに与えます。これにより、内部のFleur Contextをモックすることが出来ます。

あとは[data-testid]を元に要素を探して適切な値が出てるかを調べてるだけですね。
TestingFleurContext で囲うということだけ覚えておいてください。

次にAPIリクエストのテストをします。

Case2
  it('APIリクエストちゃんと飛ぶ?', async () => {
    const apiSpy = jest.fn(async () => void 0)
    const context = mockedContext.derive(({ injectDep }) => {
      injectDep(API.putCharacter, apiSpy)
    })

    const tree = render(
      <TestingFleurContext value={context}>
        <なんとかRouter url='/characters/1'>
        <CharacterPage />
        </なんとかRouter>
      </TestingFleurContext>
    )

    fireEvent.change(tree.getByTestId('input'), {
      target: { value: 'Haru' }
    })

    // Wait for request
    await new Promise(r => setTimeout(r))

    expect(apiSpy).toBeCalledWith({ name: 'Haru' })
    expect(context.executes[0]).toMatchObject({
      op: CharacterOps.fetchCharacter,
      args: ['1']
    })
  })
  • Operationのテストでも登場したinjectDepがここでも登場します。 Componentから外部通信を起こすようなパターンの設計をしている場合、ここでdepend / injectDepを使うことで関数をモックすることが出来ます。
  • Componentから発火されたexecuteOperationの内容は`context.

裏話

Fleurの生まれ

FleurはDelirというWeb製映像編集ソフトを開発していく過程で生まれました。

当初はFlux Utilsを使って構築されていましたが、直接的なStoreへの依存や、Action CreatorからStoreを触れないのはComponentが表示以上の責務を負わなければならないなどの問題があり、より適切な移行先を探す中でFluxibleにたどり着きました。Reduxはかなり要件のアンマッチがあり採用しませんでした。

  • Storeに副作用が持てない
    • JSONを扱うのが基本
    • じゃあレンダリングエンジンのインスタンスはどこでもつのか? Component側か?アプリケーション全体のあらゆるイベントを受け付けないといけないのにComponent? React Context使ってインスタンス配っても状態変化の予測がつかない狂気になるだけじゃん?
      • Storeに突っ込むのが保守性・予測性が良い。ならReduxは使えない…
      • レンダリングエンジンが自身で副作用を発してもComponent側からはただのStoreの変化として扱える、合理的。
  • 標準で非同期処理に対応していない
  • 型付けのためだけのコードが多くてしんどい
    • ActionNamesとActionの型定義が離れてるの果てしなくやりづらい

しかしFluxibleも2016年くらいのときには既につらみを抱えていました。TypeScriptの台頭です。
Fluxibleは上記の要件的には完全に一致しており、コードの読み書きにもほとんど困らないAPIでしたが、こと型をつけるという文脈ではかなり無理のあるAPIになっていました。(pixivFACTORYに所属していた時にFluxibleを採用し型定義を自前で行いましたが、相当しんどいものでした。)

そこでFluxibleを元に、より現代的な設計パターンやコードの書きやすいAPIを取り入れたFluxフレームワークを作る、という目的でFleurが誕生しました。

  • 型推論フレンドリー
  • コードの書き心地を良くする
  • React Hooks対応
  • 非同期処理対応 (Fluxibleは対応済み)
  • immer.js組み込みのStore
    • 大体のケースで使ったほうが良い。 まだImmutable.jsが良いとかImmer.jsがES5で使えない思ってる人いないよね?
  • パフォーマンス上のボトルネックにならない
    • Reactの再レンダリングによって映像レンダリングのFPSが落ちる。UXに如実に影響を与えるのでこれは必要な要件だった。

さらに、本業で得たSSR周りや一般的なWebアプリケーションに対する設計に関する知識を取り込み、通常のWebアプリケーションやSSR環境でも利用可能なフレームワークとしてキメキメにしていきました。

そういう経緯があり、Fleurは2019年までのSPA用ライブラリとしてはn番煎じをキメ込んで非常にまとまりが良く保守性・汎用性の高いフレームワークに仕上がりました。

SSR対応

SSR要件もちゃんと気にかけており、SSRを行うデモアプリを運用して、Apache Benchで現実的じゃない量のアクセスをかけてメモリリークが起きていない(GCでちゃんとメモリが開放されること)を確認したりしてします。

今回のサンプルにも載せられなかったんですが、ルーターも用意されています。

最後に

言い訳させてください! 完全攻略ガイドと言っておきながら体調不良によりNext.jsとかSSRとかに結局触れられませんでした!そこらへんは後日別記事でやるかもしれません(この記事のウケがよかったらね)

本記事で書いたコードはra-gg/advent-calendar-fleurにまとまっています。(実は雰囲気コードなので全部のテストは通ってないんだけどね)

とりあえず自分の中では2019年までのSPA設計のまとめとしてはいい感じのフレームワークになっているかなと思います。残念ながら社内プロダクトでの採用の機会はまだないんですけどね〜! 来年は2020年なのできっとさらに色々良くなったFleurをお見せできるかと思います!

もしよろしければ、「雑にFleur使ってみた」「作者が怠惰だからオレがFleur完全攻略してみた」など、Fleurに対する感想、不満、オレの設計語りなどインターネットに放流してください! Fleur開発に対するモチベーションと設計の洗練度が上がるので! Twitterハッシュタグは #fleurjs GitHub Topic / Qiita tagは fleur-js です!

Q & A

2019年的?

2019年的です。2020年的ではないという意味でもあります。色々情報を集めてはいますがまだよくなれる余地がありそうな気がしています。マイグレーションしやすいことは考えていますが、Breaking changeは入れていくと思います。

Next.jsで使える?

使えます。いますぐyarn create fleur-next-app <your-app-name>しましょう。
本記事で解説したのとほぼ同等の構成のファイル郡でアプリ開発をおっ始める事ができます。

そのうちNext.js + Fleurでアプリを作る記事でも書こうかと思います。

画面毎にStoreを分けるのどう思う?

いいと思う。ただその場合この記事で解説したdomain毎の分割だけだと治安が悪くなるので、page毎にStoreを入れるディレクトリを上手く分ける必要はありそう。ただし現バージョンのFleurはあまりそれがやりやすい構造になってないので、それについては今後の課題かなと思っています。

useReducerを薄〜くラップしたライブラリがあるとそういう「アプリ全体には影響ないけど、ある特定のページ以下でめっちゃグローバル」みたいなやつを切り出せるんだよな〜とも思っています。これも考えてる。既にいくつかそれらしいライブラリがあった気もしている。

なんでOperationとActionが分かれてるの?

ActionとOperationが1ファイルに混じってごちゃごちゃするのが嫌だったのと、Re-ducksパターンによる影響と、実際ActionとOperationってそんなに綺麗に一対一になるか?という気持ちに決着がつけられていないためです。typescript-fsaはその点すごくシンプルですね。ActionとOperationが透過的に扱えるRedux故の特徴です。 Fleurも何かしら考えたほうがいいよな〜とは思っています。

middlewareない?

ない。middlewareを入れると実装の見通しが悪くなるので、簡単に入れられる仕組みを入れたくない。どうしても必要だったらcontextをラップしてくれ。(Redux DevTools対応はその方式でやってる)

const yourMiddleware = (context: AppContext) => {
  const executeOperation = context.executeOperation
  const dispatch = context.dispatch

  context.executeOperation = (op, ...args) => {
    // なにがしの処理をする
    context.dispatch(MiddlewareActions.hoge, {})
    return executeOperation(op, ...args)
  }

  context.dispatch = (action, payload) => {
    if (action.name.indexOf('.started')) {
      // ローディングを出したりする
    }

    return dispatch(action, payload)
  }

  return context
}

const app = new Fleur();
const context = yourMiddleware(app.createContext())

Fleurのselectorはmemoizeしてる?

してない。 関数一発でmemoizeしてもキャッシュヒット率がたかが知れてるのでしてない。

代替案としてはReactのuseMemoを使う形に寄せて欲しい。useMemoならコンポーネントの文脈に沿ったよりヒット率の高いmemoizeができる。

reselectのコードを読んだことがあればわかると思うけど(ここらへんな)、あるselectorが複数回異なる引数で呼ばれるような場合、reselectのmemoizeはまともに機能しないっぽく見える。ある引数でmemoizeしても次に異なる引数でselectorが呼ばれればキャッシュは破棄される。そして真面目にプロダクトコード書いてたらこれは普通に発生する。

EcmaScriptのRicher keys - compositeKeyが入ればWeakMapで今よりはマシなmemoize機構を作れそうだけど、FleurもReduxも別々の問題でキャッシュヒット率には難がありそうな予感がしています。

なんでComponentからdispatch出来ないの?

dispatchはStoreに対するかなり低レイヤーな操作です。もしこれをComponentから使えるようにしてしまうと、アプリの構造化が属人的になりやすくなってしまいます。(どこからどういう粒度でdispatchするかを考える余地が生まれてしまう)

とはいえ昨今のイケてるフロントエンドを見ているとReact SuspenseとかuseAsyncなどを使ってComponent側から通信を起こすケースがありがちなので、そろそろ許しても良いかもしれない…けどアプリが大きくなってもそれを続けられるのか…?小規模アプリだから許されてるのでは…? そもそもその手のアプリはFluxライブラリ使ってないんじゃ…? というところで悩んでいます。

俺はVue派なんだが?

Vue版も作ろうか悩んでる、やればVuexより型は強くなる。ただ内部事情としてimmer.jsとVueは相性が悪いとかいう次元じゃないのでFleurの書き直しかMutableStoreの対応が必要になる。
あとMobxとかVuexとか色々見ていて「Fleurは本当にこのままの設計でいいのかな〜?」という気持ちもあるのでそこらへん全てに踏ん切りがついたらやるかもね

(たぶんVueの人たちVue公式以外のもの使いたがらなさそうという気がするので暇すぎてひまわりになったら?やる)

パフォーマンスどんなもん?

ちょっと極端なケースで計測しているのですがFleur vs Reduxはこんな感じです。(masterの最新コミットでの比較)

image.png

Fluxibleも計測してるんですがあまりにも遅いしwarnが多いので省きました。

なんで君のサンプルコードexport defaultしてないの?

  • Named exportしておくとVSCodeのimport候補に出やすくなるから
  • 後から「あっこの名前よくなかったわ」って時にVSCodeの自動リファクタリングで一発で名前を変えたいから
    (default importされたやつは名前変更で一発で変わってくれない)
  • default importした時に、人によってimport物にどういう規則で名前つけるか揺れてほしくないから
    • import名に対してコーディング規約を考えるのしんどいから最初から適切な名前ついていてホシイ ? ?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】useEffectの第2引数って?

概要

ReactのHooksに関して学び始めた際にuseEffectの使用方法、特に第2引数部分の意味の理解に苦しんだので、そのまとめです。

useEffectとは

ReactのHooksの1つです。
公式では、副作用を実行するフックのように説明されています。
ざっくり言うと、ライフサイクルメソッドを関数コンポーネントで実現する為に使われたりします。

このuseEffectですが、2つの引数を持ちます。

まず第1引数

デフォルトではRender毎に実行される関数を取ります。

useEffect(() => {
  console.log("毎回実行");
});

そして第2引数

第2引数を与える事で第1引数の関数が実行されるタイミングを自在にコントロールする事ができます。
下記の2つのケースを考えます。

(1) 空の配列が渡された場合

第2引数に空の配列が渡された場合、マウント・アンマウント時のみ第1引数の関数を実行します。

// マウント・アンマウント時のみ第1引数の関数を実行
useEffect(() => {
  console.log('マウント時のみ実行')
}, [])

(2)値の配列が渡された場合

第2引数に値の配列が渡された場合、最初のマウント時と与えられた値に変化があった場合のみ第1引数の関数を実行します。

// 与えられた値に変化があった場合のみ第1引数の関数を実行
useEffect(() => {
console.log('変化があった場合の実行')
}, [value])

まとめ

  • 第2変数を指定なし=> Render毎に第1引数の関数を実行。
  • 第2変数に[]を指定 => マウント時とアンマウント時に第1引数の関数を実行。
  • 第2変数に値の配列を指定 => マウント時と指定された値に変化があった場合のみに第1引数の関数を実行。

参照

本記事では非常に簡単なuseEffectの説明を行いました。
より詳しく、正確な詳細等は下記を参照してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactとNginxでリロードしても404しないSPAを作る

はじめに

いなたつアドカレの二十四日目の記事です。
Dockerを使ってReactで作ったアプリケーションをnginx上にポイ投げするためのあれですね。

DockerってなにとかnginxってなにReactってなにってのはスルーで行きます。
今回の記事はcreate-react-app(以下CRA)を使用する前提となっています。

つかうもの

  • react
  • react-router
  • nginx
  • docker

完成系

react-router使ってnginx上でリンクに#がつかないかつリロードしても404にならない構成の作成

ディレクトリ構成

  • app
    • docker
      • react
        • Dockerfile
      • nginx
        • default.conf
    • web
    • docker-compose.yml

こんなかんじです

dockerディレクトリにdockerで使用するファイルを格納しています。
webディレクトリにCRAで作成したアプリが入りますね。

DockerでReact

docker-compose.yml
version: '3'

services:
  react_app:
    container_name: react_app
    build: ./docker/react
    command: npm start
    volumes:
      - ./web:/app
    ports:
      - 3000:3000

つづいて
docker/react/Dockerfile

FROM node

WORKDIR /app

ここは基本的になんでも構いません(めんどくさかった)
作成するアプリケーションに合わせてpackage.jsonなどを用意してあげてください。
今回は めんどくさいので これでいきます。

Dockerで んぎっくす

はい、Nginxいきます

docker-compose.yml
nginx:
    image: nginx
    container_name: nginx
    ports:
      - 8080:80
    volumes:
      - ./web/build:/var/www
      - ./docker/nginx/:/etc/nginx/conf.d/
    depends_on:
      - react_app

web(CRAのディレクトリ)の中のbuildをnginxコンテナにマウントしています。
docker/nginxには設定ファイルですね。

default.conf
server {
    listen       80;

    location / {
        root   /var/www;
        index  index.html index.htm;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

buildをマウントしてそのマウントしたファイルのindex.htmlを表示するぜ〜って感じですね。

Reactのコンポーネント構成

App.js
import React from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom'
import Hello from './Pages/Hello'
import Changed from './Pages/Changed'




function App() {
  return (
    <Router>
        <Route exact path='/' component={Hello} />
        <Route exact path='/changed' component={Changed} />
    </Router>

  );
}


export default App;

react-routerで2つのページを遷移できるようにします。

/Pages/Hello,js
import React  from 'react';
import {Link} from 'react-router-dom'

const Hello = () => {

    return (
        <div>
            <div>Hello</div>
            <Link to='/changed'>ぺーじせんい</Link>
        </div>
    )
}

export default Hello;
/Pages/Changed,js
import React from 'react';

const Changed = () => {

    return (
        <div>
            <div>Changed</div>
        </div>
    )
}
export default Changed;

ページ遷移するだけですね。

うごかしてみる

とりあえずreactのプロジェクトをbuildしましょう。

localhost:8080にアクセスします。

スクリーンショット 2019-12-24 14.05.23.png

こんな感じですね。

ページ遷移してみます。

スクリーンショット 2019-12-24 14.05.29.png

遷移できましたね。おっけーです。

ちょっとリロードしたくなってきたわ

スクリーンショット 2019-12-24 14.05.34.png

あっあっあっ

だめですね。んぎっくすさんに怒られちゃいました。

ハッシュルーターとかダサいじゃん?

URLに「#」とかつくHashルーターをつかうことでこれを解決することはできます。

localhost:8080/#/changedだっっっっっっっっっさあああああい

やだよ。

解決しよう

nginxの設定を少し変えましょう
```default.conf
server {
listen 80;

location / {
    root   /var/www;
    index  index.html index.htm;
    try_files $uri /index.html;
}
error_page   500 502 503 504  /50x.html;
location = /50x.html {
    root   /usr/share/nginx/html;
}

}
```

try_files $uri /index.html;この一行を書くだけですね。

ほら、リロードしてみてくださいよ、怒られますか?怒られませんよね。

これで解決ですね。

なぜ解決できたのか

とりま解決でけたらえーねんって人はみなくてよきですえ

nginxにはtry_files ディレクティブというものが存在し、これは、引数を前から順番にファイルが存在するかをtryしまくって行ってくれます。そして見つからなかった場合はindex.htmlを返却しましょうねっていう感じです。

何も見つからなかったら404を返すよ〜って実装をする場合もありますね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SCSSとstyled-componentsの勉強と比較のための環境を作った

Make IT Advent Calendar 2019 24日目の記事です。

今年も危うくクリぼっちになりかけましたが、サークルの仲間とクリパすることになりました。
皆様も良いクリスマスをお過ごしください。

今年は、マイCSSブーム到来というやつです。5日目にもCSS関連の記事を投稿しました。
ゼミやインターンでSCSSを使っていますが、少し前からstyled-componentsに興味が湧いたので、勉強がてらどっちも書いてみようという旨です。

差分が発生する箇所について、コードを書いた感想を述べていくだけですので、実行環境だけ欲しい方は 環境 > リポジトリ(github) をクリックして git clone してください。

環境

  • リポジトリ(github)
    • 投稿時現在では、矢印アイコン・メニューボタン・クレジットカードコンポーネント(作り途中)が含まれています。
  • 言語・ライブラリ
    • typescript(v3.7.2)
    • react(v16.12.0)
    • storybook/react(v5.2.8)
    • scss(v4.13.0)
    • styled-components(v4.4.1)
  • 実行環境
    • macbook pro 2015 catalina
    • google chrome
  • エディタ
    • visual studio code
  • vscode plugin
    • vscode-styled-components
      • テンプレートリテラル内でcssの補完が効きます
    • color highlight(他にオススメあればご教授ください)
      • テンプレートリテラル内でカラーコードの背景に色が出なかったので導入しました

差分

webpack.config.js

SCSS
module.exports = {
  /* 一部省略 */
  module: {
    rules: [
      {
        test: /\.(ts|tsx)?$/,
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: ["@babel/preset-env", "@babel/react"]
            }
          },
          "ts-loader"
        ]
      },
      {
        test: /\.css?$/,
        use: ["style-loader", "css-loader"]
      },
      {
        test: /\.scss?$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              url: false,
              sourceMap: true,
              importLoaders: 2,
              modules: true
            }
          },
          {
            loader: "postcss-loader",
            options: {
              sourceMap: true,
              plugins: [require("autoprefixer")({ grid: true })]
            }
          },
          {
            loader: "sass-loader",
            options: {
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new TypedCssModulesPlugin({
      globPattern: "src/**/*.scss"
    })
  ],
  /* 一部省略 */
};

scssのために4つのloaderを通しています。おかげでconfigが縦に長い!
autoprefixerしか使っていないので良いですが、今後を考えるとpostcss.config.jsとして別ファイルにした方が良いですね。

TypedCssModulesPlugin はscss保存時、自動でd.tsを生成します。dropbox製というのが好きです。

私は、フォルダをコンポーネント名にして、中にindex.tsxを作成する方法を使います。
これは、コンポーネントに関係する style.scss, style.d.ts, react custom hooksをまとめてフォルダ内に置けるので気に入っています。

styled-components
module.exports = {
  /* 一部省略 */
  module: {
    rules: [
      {
        test: /\.(ts|tsx)?$/,
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: ["@babel/preset-env", "@babel/react"],
              plugins: ["babel-plugin-styled-components"]
            }
          },
          "ts-loader"
        ]
      }
    ]
  },
  /* 一部省略 */
};

スッキリしました。
autoprefixer・TypedCssModulesPluginの役割は、styled-componentsが全て担ってくれます。
babel-plugin-styled-components について、こちらの記事が参考になりました。

reset.css

// for SCSS
import 'reset-css';

// for styled-components
import React from 'react';
import { Reset } from 'styled-reset';

const App = () => (
  <>
    <Reset />
    <div>Hello world!</div>
  </>
);

SCSSでは shannonmoeller/reset-css を使用します。
styled-componentsでは zacanger/styled-reset を使用します。

ソースコード比較(buttonコンポーネント)

See the Pen menu button scss by haduki1208 (@haduki1208) on CodePen.

マウスホバーでアニメーションするボタンです。
これをstyled-componentsで置き換えます。

See the Pen menu button styled by haduki1208 (@haduki1208) on CodePen.

scssがなくなったことでフォルダ内がスッキリしますが、scssの内容がtsxに移動するのでファイルが縦に長くなります。
私はvscodeのエディタを分割してtsxとscssを並べながらコードを書くので、辛さがあります。
↓こんな感じ
Image from Gyazo

解決策として、
1. atomic designを意識してコンポーネントを小さくしていく
2. style.tsx のようなファイルを用意してstyledの要素を宣言する
を考えました。基本的に1番の方法を実践していきたいと思います。

おわりに

趣味で何かコーディングするとき、このリポジトリ(github)を使っていこうかなと思っています。
storybookも始めて導入してみたのですが、どんなコンポーネントがあるか一通り見れるので便利です。

scss(css)で培ってきた技術がそのままstyled-componentsに活用できるため、
新規プロジェクトはstyled-componentsだったらいいなぁと思っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript で自分だけの React を作る

この記事は 株式会社 ACCESS Advent Calendar 2019 24 日目の記事です。

先週、@Momijinn (全くの別人でした。失礼しました。) 社内のとあるフロントエンダーに「Build your own React」という記事の存在を教えてもらいました。React の内部実装を把握するためにスクラッチで自分の手を使って実装する手順を紹介している記事です。

元記事は JavaScript で記述されていたのですが、そのまま写経するのもつまらないので TypeScript に書き換えながら実装してみました。元記事の部分的な日本語訳や実装しながら得た知見をこの記事で紹介したいと思います。
この記事は実装の最終形を機能ごとに分割して説明しますが、元記事は React を構成する重要な要素を順番に紹介しています。深い理解を得たい方は元記事を読みながら自分の手で実装することがおすすめです。
自分が実装したコードは こちら で公開してます。

今回実装した React の API は
- React.createElement
- ReactDom.render
です。

動作環境
requestIdleCallbackString.prototype.startsWith が動作するブラウザであることが前提です。

JSX (TSX) と DOM

公式 でも言及されていますが、JSX の記述は React.createElement のシンタックスシュガーです。JSX を使用した記述は下記例のように書き換えられます。

JSX
const hello = <div id="hoge">Hello {this.props.toWhat}</div>;
JavaScript
const hello = React.createElement('div', { id: 'hoge' }, `Hello ${this.props.toWhat}`)

React.crateElement の引数は下記のようになっています。

JavaScript
React.createElement(
  type,         // タグ名の文字列
  [props],      // タグに付与する属性
  [...children] // 子要素
)

では TypeScript で React.createElement を実装してみます。

TypeScript
const TEXT_ELEMENT = 'TEXT_ELEMENT' as const
type TagType = string // ここの拡張を頑張るとタグの型定義ができる
type ElementType = typeof TEXT_ELEMENT | TagType

interface Props {
  nodeValue?: string
  children: Element[]
  [key: string]: any // タグに付与する属性が入る
}

interface Element {
  type: ElementType
  props: Props
}

const createTextElement = (text: string): Element => ({
  type: TEXT_ELEMENT,
  props: {
    nodeValue: text,
    children: []
  }
})

const createElement = (type: ElementType, props: Props, ...children: Element[]): Element => ({
  type,
  props: {
    ...props,
    children: children.map(child => (typeof child === 'object' ? child : createTextElement(child)))
  }
})

crateElementを実行すると、children に対して再起的にcreateElementを実行します。そして子要素を持たない末端のノードに対してはcreateTextElementを実行します。

下記に実行例を挙げます。

const sample = (
  <div id="foo">
    <a href="/" target="_blank">
      link
      <span id="baz">hoge</span>
    </a>
  </div>
)

const sample = {
  type: 'div',
  props: {
    id: 'foo',
    children: [
      {
        type: 'a',
        props: {
          href: '/',
          target: '_blank',
          children: [
            { type: 'TEXT_ELEMENT', props: { nodeValue: 'link', children: [] } },
            {
              type: 'span',
              props: { id: 'baz', children: [{ type: 'TEXT_ELEMENT', props: { nodeValue: 'hoge', children: [] } }] }
            }
          ]
        }
      }
    ]
  }
}

のような構造に変換してくれる関数がcreateElementです。
おそらくこの Element のツリー構造がいわゆる仮想 DOM に当たるのだと思います。React 開発者から見る・操作することができる DOM ツリーが上記 Object っぽいです。

こうして変換された Element を DOM に反映させるためのcreateDOMを実装します。

TypeScript
const isEvent = (key: string) => key.startsWith('on')
const isProperty = (key: string) => key !== 'children' && !isEvent(key)

const toEventType = (key: string) => key.toLocaleLowerCase().substring(2)

const createDom = (element: Element) => {
  if (element.type === TEXT_ELEMENT) {
    return document.createTextNode(element.props.nodeValue!)
  }

  const dom = document.createElement(element.type)

  Object.keys(element.props)
    .filter(isEvent)
    .forEach(name => dom.addEventListener(toEventType(name), element.props[name]))

  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => dom.setAttribute(name, element.props[name]))

  return dom
}

子要素を持たない末端のノードは TextNode として生成します。
末端のノード以外は通常の HTMLElement として生成します。on から始まるイベント属性をaddEventListenerで設定し、その他の属性はsetAttributeで設定します。

JSX (TSX) -> DOM 生成までの繋ぎこみが上記の実装により完了しました。

レンダリング

生成された DOM を実際に描画させます。描画に使用するのが requestIdleCallback です。ブラウザがアイドル状態の時に、既に描画済みの DOM に子として生成された DOM を追加します。

TypeScript
export type Fiber =
  | ({
      dom: HTMLElement | Text
      parent?: Fiber
      child?: Fiber
      sibling?: Fiber
    } & Element)
  | null

const requestIdleCallbackFunc = (window as any).requestIdleCallback

let nextUnitOfWork: Fiber = null
let wipRoot: Fiber = null

const commitWork = (fiber: Fiber) => {
  if (!fiber) {
    return
  }

  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

const commitRoot = () => {
  commitWork(wipRoot.child)
  wipRoot = null
}

const workLoop = () => {
  while (nextUnitOfWork) {
    // performUnitOfWork() は後述
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
  }

  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }

  requestIdleCallbackFunc(workLoop)
}

requestIdleCallbackFunc(workLoop)

ここで Fiber なる概念が登場します。上記の Element の要素に加えて、DOM の実体、親・子・兄弟 Fiber への参照を持っています。React の描画処理は Fiber を元に実行されます。
現時点の実装ではブラウザがアイドル状態になった時にworkLoopを呼びだし、全ての DOM を更新しています。差分検出(Reconciliation)は未実装です。
performUnitOfWorkは差分検出を実装することを見据えて分割して呼び出せるようになっています。

TypeScript
const performUnitOfWork = (fiber: Fiber) => {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  const elements = fiber.props.children
  let index = 0
  let prevSibling: Fiber = null
  while (index < elements.length) {
    const element = elements[index]

    const newFiber: Fiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null
    }

    if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }

  if (fiber.child) {
    return fiber.child
  }

  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }

  return null  
}

performUnitOfWorkは何をやっているのかというと

  • Fiber に DOM の実体がなければ生成
  • 次に実行する Fiber を返す
    • 子 Element があれば子 Fiber を生成して返す
    • 子 Element がなく、兄弟 Fiber があれば返す
    • 子 Element・兄弟 Fiber がなく、親の兄弟 Fiber があれば返す
    • 以降、親要素の兄弟 Fiber を探し続ける。なければ null を返す

上述したように、Element を Fiber 単位に基づいて DOM の描画処理を行います。

最後にrender関数を実装します。

const render = (element: Element, container: HTMLElement) => {
  wipRoot = {
    type: container.tagName,
    props: {
      children: [element]
    },
    dom: container
  }
  nextUnitOfWork = wipRoot
}

const sample = (
  <div id="foo">
    <a href="/" target="_blank">
      link
      <span id="baz">hoge</span>
    </a>
  </div>
)

const container = document.getElementById('root')
render(sample, container)

これで描画まで実行されるようになりました。

疲れた

まだ差分検出と Funciton Component を紹介していませんが、ここで一旦終わりにします。「差分検出がなくて何が React なんだ!」との声もあるでしょうが、勘弁してください。年内を目標に続きの記事を書きます。実装自体は完了しているので興味がある方は こちら を見てください。

最後に

TypeScript 最高です。バニラの JS だと写経でも写し間違えに気づかず詰んでいた可能性すらあります。リファクタリングや思考の整理にも、型があるとないとでは捗りに天地の差があると思います。なるべく新規の案件には導入していきましょう!

明日は @naohikowatanabe さんで「要素の表示非表示は visibility:hidden の方が display:none よりも高速」だそうです。お楽しみに。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactJS react-routerのサンプルアプリ

非常にシンプルな実装例のメモ!
github: https://github.com/Kohei-Sato-1221/SugarReactRouter

スクリーンショット 2019-12-24 12.24.15.png

react-routerとは?

Reactでルーティングを実現するためのライブラリ

 前提条件

  1. npmコマンド使えるようにしておく
  2. create-react-appを使えるようにしておく

実装方法

サンプルのReactプロジェクトを用意

create-react-app router-sample

ライブラリをインストール

cd router-sample
npm install react-router-dom

App.jsを以下の通り書き換える

App.js
import React from 'react';
import {Route, BrowserRouter, Link} from 'react-router-dom'

const App = () => (
  <BrowserRouter>
    <div>
      <div><Link to='/page1'>Go to Page1</Link></div>
      <div><Link to='/page2'>Go to Page2</Link></div>

      <br/>

      <Route path='/page1' component={Page1} />
      <Route path='/page2' component={Page2} />
    </div>
  </BrowserRouter>
)

const Page1 = () => (
  <div>This is Page1</div>
)

const Page2 = () => (
  <div>This is Page2</div>
)

export default App;

以下のコマンドで起動

npm start
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GraphCMS から入り、Absintheを利用して作って動かす「チュートリアル」

この記事は、「Elixir Advent Calendar 2019」の24日目になります。

「Elixir Advent Calendar 2019」23日目は、@sanpo_shihoさんのElixirで作るニューラルネットワークを用いた手書き数字認識①でした。

そしてこの記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar 2019」の1日目から始まった3部作の3つ目の記事で、Advent Calendar NervesJP 6日目の続きになります。

東京だけど fukuoka.ex の YOSUKENAKAO.me です。

普段は合同会社The Waggleで「教育」に関わるサービス作りのお仕事と学習教材の開発や
研修講座の企画開発をしています。

この記事の構成
Advent Calendar fukuoka.ex 1日目
https://qiita.com/advent-calendar/2019/fukuokaex
GraphCMS から Absinthe を利用して作る Elixir で体験的に GraphQLSever を作る「ポエム」

Advent Calendar NervesJP 6日目
https://qiita.com/advent-calendar/2019/nervesjp
Nerves と GraphQLsever の組み合わせを考える「ポエム」

Advent Calendar Elixir 24日目
https://qiita.com/advent-calendar/2019/elixir
GraphCMS から入り、Absintheを利用して作って動かす「チュートリアル」

となっています。

対象者はプログラミングを始めたい人で、新しい技術を学びたい人

逆を言えば、このチュートリアルができない人で、プログラミングを学びたい人は
Elixir |> Collegeの対象者です。

また、このチュートリアルは完了して、日本語で学びたい人は TheWaggleのオンライン教材の対象者(2020年春頃ローンチに向けて作成中)です。

注意点

このチュートリアルは、最短で何を学ぶべきかと、流れを通してそれぞれの技術的な繋がりを把握してもらう為のものです。

独学ができる人に向けた最初の入り口です。
PHP? Python? Ruby? がやりたいだって?やりたい勉強すりゃいいよ。ただし一言だけ言っておく、Elixirは良いぞ(笑)

  • HTML,CSS,JavaScript
  • React
  • GraphCMS
  • Elixir
  • Phoenix
  • Ecto
  • GraphQL(Queryのみ)
  • Apollo
  • Absinthe
  • SQL
  • mix

チュートリアルの流れ

  1. GraphCMSのアカウント登録
  2. GraphCMSでModelの作成
  3. Reactの環境構築(Yarnが使える人は飛ばす)
  4. サンプルプログラムのダウンロードと作成
  5. サンプルデータの作成
  6. GraphCMS Endpointの設定
  7. ElixirでGraphQL Server構築

1.GraphCMSのアカウント登録

下記のサイトへアクセスしてください。
https://graphcms.com/

330af3a7-3a8c-52e5-1b79-e6fcb62fcfcd.png

  1. SignUpよりユーザー登録をします。
  2. その後、ログインして下さい。
  3. Login後、「Create new project」をクリックします。
    ae4f442c-2688-af08-0376-f60b318c919e.png

  4. 「Form Scrach」を選択します。

bdd26e33-0720-7b54-77a6-f598b079e763.png

5. 「Asia East」のリージョンを選びます。

f7a63dbe-f92b-c924-2d71-4d8b5c8938e9.png

6. Pearsonalプランを選択し、Continueを選びます。

9536e393-68dc-62a2-e802-5a82154136f0.png

7. モデルの作成から順に行って行きます。

f109de0f-aeae-aa14-37ac-8d633a176c7f.png

2. GraphCMSでModelの作成

8. Schemaの設定

Schemaと書かれたタイトルの下にModelsと書かれていてサイドに[+]のボタンがあります。

743a34b0-4370-1bb9-150b-43d4d9820eb0-1.png

9. Create Modelで「Display Name」の欄にAuthorと入力し、Create Modelを押します。

0e2b3e5b-5235-cd3f-2968-0b4c4211518c.png

10. 右側にFIELDSと書かれたタブが表示されるので、クリックします。

c781f204-fc61-5ae0-fa5a-19a6bb89127b.png

11. 様々なFiledsのタイプがあります。今回は、Single Textをドラッグしてドロップします。

9423fd1c-d442-ae67-c120-ffd0e32319c2.png

12. Display Nameに nameと入力します。

88bbaeed-7744-3e01-3e32-9d5bd197eb52.png

13. フィールドタイプが追加されるとこのような形になります。

b855a074-78fc-af15-7912-7861cda50ee8.png

同じ要領で、Authorのフィールドは以下のように設定します。

スクリーンショット 2019-12-23 20.15.57.png

14. Authorを設定したら新しく、ModelをPostで作成します。フィールドは以下のようにします。
スクリーンショット 2019-12-23 20.16.37.png

3.Reactの環境構築(Yarnが使える人は飛ばす)

NodeJSのインストール

https://nodejs.org/ja/

LTS版と最新版があります。 LTS版をインストール

インストールの確認

コンソールを再起動します。

node --version

次のv12.13.1ようにバージョン番号が表示されていたらインストール完了です。

npm --version

次のv6.12.1ようにバージョン番号が表示されていたら成功です。

Yarn install

https://yarnpkg.com/ja/docs/install#mac-stable

Windowsの場合は、yarn のインストーラーをダウンロードできるボタンが表示されるので
それをダウンロードしてインストールして下さい。

4. サンプルプログラムのダウンロードと作成

https://github.com/GraphCMS/graphcms-examples/tree/master/current/react-apollo-blog

上記のサイトに下記コマンドを実行と記載があるので、下記のコマンドを実行します。

git clone https://github.com/GraphCMS/graphcms-examples.git && cd graphcms-examples/current/react-apollo-blog && yarn && yarn start

実行すると、ブラウザでlocalhost:3000で以下のサイトが見れたら成功です。

blog_.png

5. サンプルデータの作成

  1. 「content」ボタン(三本の横棒マーク)をクリックし、データを入力したい、「Model名」(画像の例は、Author)を選びます。
  2. 「Create New」をクリックすると、入力フィールドが表示されるので、適当な値を入れて「Published」を選んで「Save」します。

※Postにも忘れずに、同じ要領でサンプルデータを作成してください。

sample_data_set.png

6. GraphCMS Endpointの設定

設定から、Endpointをコピーします。

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f37323936322f66343739643633622d663938342d363839382d323733302d3761666335353961633036362e706e67.png

Endpointをコピーしたら、設定より、PermissionsのScopeを「PROTECTED」から「QUERY」に変更します。

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f37323936322f36613236663035302d326638342d643965322d653162302d3631366139633264393532342e706e67.png

Endpointをプログラムに反映する

react-apollo-blog/src/index.js
// Replace this with your project's endpoint
 const GRAPHCMS_API = 'ここに先ほどコピーしたEndpointのURLを入れる'

index.jsの全体像
スクリーンショット 2019-12-24 10.12.43.png

ここまできたら yarn startで無事に自分の入力したデータが表示される事を確認します。

ここからは、こちらのブログで書いた手順の5番目以降の説明と同じものになります。
https://qiita.com/Yoosuke/items/2346137ac39715b19cde

6. Absinthe を使用して GraphQL のアプリをセットアップします。

mix.exs
defp deps do
 [
   # existing dependencies

   {:absinthe, "~> 1.4.16"},
   {:absinthe_plug, "~> 1.4.0"},
   {:absinthe_phoenix, "~> 1.4.0"}
 ]
end
mix deps.get

7.sampleBlog_web に schema フォルダを作成し、schema.ex ファイルを作成して、ファイルを作成します。

lib/sampleBlog_web/schema/schema.ex
defmodule SampleBlogWeb.Schema.Schema do
    use Absinthe.Schema

    query do
      @desc "Get a list of authors"
      field :authors, list_of(:authors) do
        resolve &SampleBlogWeb.Resolvers.Blog.authors/3
      end

      @desc "Get a author by its id"
      field :author, :author do
        arg :id, non_null(:id)
        resolve &SampleBlogWeb.Resolvers.Blog.author/3
      end
    end

    object :author do
      field :id, non_null(:id)
      field :name, non_null(:string)
      field :bibliography, non_null(:string)
    end
  end

8.リゾルバモジュールファイルを作成します。 sampleBlog_web に resolvers フォルダを作成し、blog.exファイルを作成して、ファイルを作成します。

lib/sampleBlog_web/resolvers/blog.ex
defmodule SampleBlog.Resolvers.Blog do
  alias Getaways.Blog

  def authors(_, _, _) do
    {:ok, Blog.list_authors()}
  end

  def author(_, %{id: id}, _) do
    {:ok, Blog.get_author!(id)}
  end
end

9.スキーマとリゾルバの準備をしたら、ルーターを設定します。

sampleBlog/lib/sampleBlog_web/router.ex
defmodule SampleBlogWeb.Router do
defmodule SampleBlogWeb.Router do
  use SampleBlogWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/" do
    pipe_through :api

    forward "/api", Absinthe.Plug,
      schema:  SampleBlogWeb.Schema.Schema

    forward "/graphiql", Absinthe.Plug.GraphiQL,
      schema:  SampleBlogWeb.Schema.Schema,
      interface: :simple
  end
end

最後にサーバーを起動してみます。

mix phx.server

これでlocalhost:4000/graphiqlにアクセスして以下の画面が出てきたら作成準備完了です。

キャプチャ.PNG

GraphQLのクエリを書く

query {
  authors{
     id
   name
    bibliography
  }
}

クエリを書いて、無事に成功していれば下記のようなデータが返ってきます。

graphql_1.PNG

React のreact-apollo-blog のAbout.jsに上記のクエリを上書きする

src/components/About.js
export const authors = gql`
query authors {
  authors{
    id
    name
    bibliography
  }
}
`

エンドポイントを書き換える

src/index.js
const GRAPHCMS_API = http://localhost:4000/api/

これで、yarn startしてlocalhost:3000/aboutページにアクセスすると、、、見れません。
エラーを確認するとクロスサイトスクリプティングの問題でデータが取得できてないです。そこで、GraphQL server側に機能を追加します。

Cros_plugを追加する

https://hex.pm/packages/cors_plug

mix.exs
  defp deps do
    [
# ~省略
      {:absinthe, "~> 1.4.2"},
      {:absinthe_plug, "~> 1.4.0"},
      {:absinthe_phoenix, "~> 1.4.0"},
      {:cors_plug, "~> 2.0"},     #<- 追加
    ]
  end

機能を追加する。

$ mix deps.get

router.exのパイプラインにプラグを追加

lib/sampleBlog_web/router.ex
  pipeline :api do
    plug CORSPlug, origin: "http://localhost:3000" #<-追加
    plug :accepts, ["json"]
  end

これで、http://localhost:3000/aboutにアクセスで以下のようにデータが取得できたら成功です。

blog.PNG

最後に

いかがだったでしょうか、あまりにも長いのでもしかしたら、抜け漏れがあるかもしれません。
見つけたら遠慮なく、おしらせ下さい。喜んで修正したいと思います。

よければ、いいね。よろしくお願いします。励みになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React × Firebase でエンジニア向け特化型SNSを開発しています

これは、ひとり開発 Advent Calendar 2019 の23日目の記事です。

はじめに

新卒( 2019年12月で退社 & 転職活動始めます!)でエンジニアをやっている筆者が、友人と一緒にここ数ヶ月開発しているサービスについて書きたいと思います。ちなみに、2020年1月中にα版をリリース予定、3月1日にはこのサービス開発についてまとめた本を技術書展で出版します。みなさん、技術書展の会場でお会いしましょう。

開発中サービスについて

エンジニアのためのプラットフォーム: 「Jeeek (ジーク) 」

サービス名についてですが、色々あってとりあえずこの名前にしています。由来はあるのですが今回の記事では省略します。

概要を最初にざっくり言うと、エンジニアの活動 (学習・転職) を支援するSNSライクのプラットフォームです。詳しくは、以下で述べていきます。

Why?<なぜ作ったのか>

まずは、なぜJeeek(ジーク)を開発・リリースしようと考えたのかを書いていきます。

活動共有、コミュニティ形成の需要があると感じたから

Twitterでは、 「#100DaysOfCode」 や 「#未経験エンジニアと繋がりたい」 といったハッシュタグをよく見かけた時期がありました。これらは、自身の活動の共有を行いたい・共有することで自分にプレッシャーをかけ継続させたい、同じような境遇の人同士で切磋琢磨し合いながら成長していきたい、などの想いがあると思います。また、他分野からIT業界への流入、プログラミング義務教育化など、時代の潮流と共にITエンジニアの増加が想定されます。

しかし、エンジニアが対象とする技術分野は幅が広く、初学者にとっては情報の取捨選択が難しいこともあり、初期は何から勉強をしたら良いのか、また実際に手を動かした後も何が分かっていないのか分からないといった状態が生まれやすいです。経験の浅いエンジニアと経験豊富なエンジニアの間に存在する情報の非対称性を狙って不当に高額なプログラミングスクールなどの情弱ビジネスも横行しています。これでは、たとえITエンジニアの人口が増加したとしても脱初心者にとても時間がかかってしまいます。こういった現状を変え、エンジニアの活動がより身近で楽しいものになったり、相互に刺激し合う環境がオンラインで構築できたり、駆け出しエンジニアの勉強コンサルにもなるようなサービスがあったら面白いのではないかと考えサービス開発を始めました。

How?<どう実現するのか>

次に、「じゃあどうやってそれを実現していこうと考えているの?」ということについてです。基本的に、現在実装中のものはエンジニア向けに特化したSNSというイメージで、今後これに機能を追加しながらエンジニアの活動全体を支援するプラットフォームにまで拡張していく予定です。

ユーザーの活動をSNSライクのタイムラインで共有します

スクリーンショット 2019-12-23 21.41.52.png

メインとなるのがタイムライン機能です。普段の活動をテンプレートベースのシンプルな投稿で共有します。例えば、『◯◯技術書の1章を読んだ』『◯◯のイベントに参加した』などです。また、エンジニアの活動はインターネットにアウトプットされることが多いことから、外部サービスでの活動を自動収集および自動投稿も可能です。外部サービス (GitHub, Qiita, connpassなど) のAPIを利用して連携させることで、外部サービスでのアクティビティを自動で取得しタイムラインで共有します。例えば、GitHubと連携済みユーザーであれば『新しく1つのcommitをしました』のような投稿を自動で行うことができます。

また、日々の投稿のログ (投稿に設定するタグ) から、ユーザーのスキルスタックを可視化・共有します。そして、それらに基づいて特定の技術でのユーザーランキングも表示します。

What?<ユーザーはどうなるのか>

次に、「Jeeek(ジーク)のユーザーはどういうことが嬉しくて利用するの?」ということについて書いていきます。

他のエンジニアの活動を自身の活動に役立てることができる

タイムラインで活動を共有することで、他のエンジニアのアクティブな活動を知ることができ、切磋琢磨する環境ができたり、良質な記事・文献にアクセスしやすくなります。また初学者にとっては、ロールモデルを発見できる可能性が高まり、自身の勉強の指針にもなります。これに加えて、技術ベースでランキングが確認できるので、自身の立ち位置を客観的に認識することげできます。

また、ユーザーのスキルスタックが公開されるので、スカウトする/されるの機会を作ります。これらのスキルスタックは日々の活動に紐づいて自動生成されるので、定期的に手動でアップデートする必要もありません。

ちなみに、スキルスタック自動生成 + 企業とマッチングについては、LAPRASさんのサービスが有名です。これに対して、Jeeek(ジーク)はより粒度の細かい活動を共有したり、和気あいあいとしたアクティブなコミュニティ形成を実現したいため、SNSの機能に重きを置いています。

開発環境

最後に、Jeeek(ジーク)の開発環境について簡単に紹介します。

技術スタック

スクリーンショット 2019-12-23 8.01.22.png

使用技術やツールなどは全て上の図に載っている通りです。

インフラにGCP、バックエンドにGo、フロントエンドにTypeScript × Reactを使っています。ちなみに僕はフロントエンドを担当しており、TypeScript, React, Redux, Redux-Saga, Figmaなどを使用しています。

おわりに

プライベート開発では、サービス企画からスプリントプランニング、UI設計、実装、リリーススケジュール、リリース後のアップデートおよびマーケティングまで全て自分たちで考えながら実行・経験できることがとても面白いと感じています。また、それと同時にその大変さも感じ、実際にリリースや運用を行っている全個人開発者のことをリスペクトするようになりました。Jeeek(ジーク)も僕個人もまだまだ道半ばですが、引き続き精進していきます。冒頭でも述べましたが、これに関連した本を技術書展で出版するので、次は3月1日の技術書展でお会いしましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】useEffectの第2引数って?

概要

ReactのHooksに関して学び始めた際にuseEffectの使用方法、特に第2引数部分の意味の理解に苦しんだので、そのまとめです。

useEffectとは

ReactのHooksの1つです。
公式では、副作用を実行するフックのように説明されています。
ざっくり言うと、ライフサイクルメソッドを関数コンポーネントで実現する為に使われたりします。

このuseEffectですが、2つの引数を持ちます。

まず第1引数

デフォルトではRender毎に実行される関数を取ります。

useEffect(() => {
  console.log("毎回実行");
});

そして第2引数

第2引数を与える事で第1引数の関数が実行されるタイミングを自在にコントロールする事ができます。
下記の2つのケースを考えます。

(1) 空の配列が渡された場合

第2引数に空の配列が渡された場合、マウント・アンマウント時のみ第1引数の関数を実行します。

// マウント・アンマウント時のみ第1引数の関数を実行
useEffect(() => {
  console.log('マウント時のみ実行')
}, [])

(2)値の配列が渡された場合

第2引数に値の配列が渡された場合、最初のマウント時と与えられた値に変化があった場合のみ第1引数の関数を実行します。

// 与えられた値に変化があった場合のみ第1引数の関数を実行
useEffect(() => {
console.log('変化があった場合の実行')
}, [value])

まとめ

  • 第2変数を指定なし=> Render毎に第1引数の関数を実行。
  • 第2変数に[]を指定 => マウント時とアンマウント時に第1引数の関数を実行。
  • 第2変数に値の配列を指定 => マウント時と指定された値に変化があった場合のみに第1引数の関数を実行。

参照

本記事では非常に簡単なuseEffectの説明を行いました。
より詳しく、正確な詳細等は下記を参照してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

レガシー React Project で実践してきたこと

はじめに

この記事はギルドワークス Advent Calendar 2019の21日目の記事になります。

現在、約3年以上開発を続けているReactのProjectに関わっているので、ここ半年で取り組んだカイゼンの内容についてここにまとめたいと思います。

改善前(半年前)のProjectの状況

以下が半年前のProjectの状況です。
現在と構成が変わっていない部分はあります。

  • Rails + Webpacker + Reactの構成
  • React.js v16.4.x
  • Redux v4.0.0
    • ただし状態管理はreduxのstoreを使っていたり、Componentのstateを使っていたり統一感がない状態
  • 非同期処理のmiddlewareには redux-saga を使用
  • UI補助ライブラリとしてmaterial-uiとbootstrapを併用
    • Component単位で依存しているUI補助ライブラリが違っている状態

当時の課題

以下が、当時のふりかえりで上がってきたReact Projectに対する主な課題でした。

  • Reactのコードがどんどん増えてきて、状態管理もstate, props, reduxのstoreと使い分けができておらず複雑になってバグが多くなってきた
  • テストしようにもComponent間の依存がすごくて難しくなってきた
  • なので大胆な変更をしようにも状態管理まわりでバグを生みそうで怖い
  • コーティングガイドラインもちゃんと統一されていなかった
  • Reactのコードが増えてきて読み込みに時間がかかるようになってきた

こういった課題が顕著にでてきていたので、自然とReactまわりのコードを徐々に改善していく運びになっていきました。

やってきたこと

ではここ半年で実施してきた施策について挙げていきます。

TypeScriptの導入

まずは、大胆な変更を行うためにはTypeScriptによる静的解析が必須であると考えました。
ただいきなりTypeScriptを導入して型を定義していくのはかなり敷居が高いので、以下の順序で導入していきました。

  • まず既存のコードをいっきに jsからts, jsxからtsxファイルへ置換する
  • TypeScriptを 型チェックを有効にしないで コンパイラのみ有効にした状態にする
const PnpWebpackPlugin = require('pnp-webpack-plugin')

module.exports = {
  test: /\.(ts|tsx)?(\.erb)?$/,
  use: [
    {
      loader: 'ts-loader',
      options: PnpWebpackPlugin.tsLoaderOptions({
        transpileOnly: true // 型チェックしない
      })
    }
  ]
}
  • 警告が出ている箇所はロジック変更をしない形で修正を加えていく
  • 最終手段として @ts-ignore でエラーを回避
  • 依存しているライブラリの型定義をインストールしていく (@types/react等)

ここまで実施して、次の項で導入したESLintでTypeScriptの型チェック等の制限を徐々に強くしていって、段階的にコードを修正していく形になります。

参考

当時は以下の記事を参考にして、TypeScriptを軽量に導入する方法の知見を得たりしました。

ESLint + Prettierの導入

TypeScriptが導入された状態で、段階的に型を有効にしていくために、ESLintの plugin:@typescript-eslint を有効にして、以下の手順でコードを徐々に修正してく手段をとりました。

  • デフォルトの設定だと当然エラーが大量にでてしまうので、地道にエラーとなっている箇所を warn に変換して警告がでている状態にする
  • CI環境では eslint がパスする状態にする
  • 機能追加や修正が入るファイルから徐々に警告の箇所を修正する
  • state, props等型定義しやすいところから型定義していく。難しい場合は anyで回避
  • 解消した警告から eslintの設定を見直して制限を強くしていく

なお、細かなコーティングガイドラインが無いという課題に対しては、ESLintの設定で airbnb-base 等一般的なコーディングスタイルをlintに組み込むことができるので、開発メンバーのエディタにlinterとPrettierを有効にしてもらって、自動でコーディングの強制を(ゆるく)実施するようにしました。

Prettierは自動でコード整形をしてくれるのでかなり有効です。

参考

既存ComponentからなるべくState管理の排除

せっかくreduxが導入されているにも関わらず、stateで状態を保持しつつ、同じ情報を複数のComponentで引き回すようなコードがいくつかあり、それが複雑性を上げてバグを生みやすくしていました。

具体的なアンチパターンとしては、以下のようなpropsによってstateを変更するようにしているケースです。
this.state.open はpropsによって更新もされますが、Component内でも変更ができてしまうため、状態管理が複雑になってしまいます。

  constructor(props) {
    super(props);
    this.state = {
      open: this.props.open,
    };
  }

  componentWillReceiveProps(nextProps) {
    this.setState({
      open: nextProps.open,
    });
  }

こういったコードは、stateを排除してprops参照にする、あるいは reduxのstoreに openの状態をもたせるようにして、状態を一箇所で管理するようにします。

ちなみに LifeCycle関数である componentWillReceiveProps は Deprecatedになっているので積極的に排除していっています。

参考

React hooksの導入

前項のState管理の排除は既存に対する対処ですが、React HooksがReact v16.8から有効になってからは、新規で作成するファイルは積極的に React.FC を用いてステートレスな関数Componentを実現してState管理の複雑性を排除していきます。

参考

Dynamic Importの導入

Reactのコードが増えてきて、且つ機能が複数ページに跨っていたため、Dynamic Importを利用して機能単位で必要になったファイルをロードするようにして、初期ロード時間の短縮を図りました。

以下のような記述に変更してあげることで実現可能になります。

import React, { lazy, Suspense } from 'react';

const HogeContainer = lazy(() => import(/* webpackChunkName: "HogeContainer" */ '../containers/HogeContainer'));

const App = (props) => (
  <Suspense fallback={<Loading />}>
    <HogeContainer />
  </Suspense>
);

export default App;

なお、Dynamic Importは React v16.8 から有効になっています。

参考

今後の導入したいこと

ここからは、まだ導入できていないけど、今後の改善として導入を検討している施策を挙げてみました。

Redux Starter Kitの導入

Reduxは状態管理やロジックを一箇所で管理できるものの、TypeScriptによる型定義が難しかったり、学習コストがかかたっりするところが難点だなと思っています。そこで今年(2019年)の10月にv1.0がリリースされた Redux Starter Kit の導入を検討しています。

Reduxのラッパーのようなものなのですが、いくつか恩恵があります。

  • TypeScriptの型定義がしやすい
  • React Hooksを利用する前提の設計になっている
  • Sliceという機能を使ってreducerの記述を簡潔にできる

以下がreducerのサンプルコードです。reducerの関数の呼び出しもかなりシンプルになっています。

import { createSlice } from "redux-starter-kit";

export type TodoItem = {
  title: string;
  completed: boolean;
  key: string;
};

const todoSlice = createSlice({
  name: "todo",
  initialState: [] as TodoItem[],
  reducers: {
    addTodo: (state, action: { payload: TodoItem }) => {
      state.push(action.payload);
    },
    removeTodo: (state, action: { payload: string }) => {
      return state.filter(item => item.key !== action.payload);
    },
    setCompleted: (
      state,
      action: { payload: { completed: boolean; key: string } }
    ) => {
      state.forEach(item => {
        if (item.key === action.payload.key) {
          item.completed = action.payload.completed;
        }
      });
    }
  }
});

export default todoSlice;

import React from "react";
import { ListGroupItem, Button } from "reactstrap";
import todoSlice, { TodoItem } from "../../reducers/todo";
import { useDispatch } from "react-redux";

type Props = {
  item: TodoItem;
};

const TaskItem: React.FC<Props> = props => {
  const dispatch = useDispatch();
  const {
    actions: { setCompleted, removeTodo }
  } = todoSlice;
  const textStyle = {
    textDecoration: props.item.completed ? "line-through" : "none"
  };

  const completeTask = () => {
    dispatch(setCompleted({ completed: true, key: props.item.key }));
  };
  const deleteTask = () => {
    dispatch(removeTodo(props.item.key));
  };

  return (
    <ListGroupItem>
      <div className="d-flex">
        <span className="flex-fill" style={textStyle}>
          {props.item.title}
        </span>
        <div className="ml-auto">
          {props.item.completed ? null : (
            <Button color="primary" onClick={completeTask}>
              Complete
            </Button>
          )}
          <Button color="danger" onClick={deleteTask} className="ml-3">
            Delete
          </Button>
        </div>
      </div>
    </ListGroupItem>
  );
};

export default TaskItem;

参考

カスタムHooksの導入

React hooksの強みは、独自のhooksを作成して、複雑な処理を共通化して流用可能にしつつ、ステートレスな関数Componentを実現できる点です。

例えばシンプルな例ですと、reduxの特定のstore情報の呼び出しをカスタムhooks化して簡潔に呼び出すといったことも可能になります。

import { useSelector } from "react-redux";
import { CombineState } from "../index";

export const useTodoItems = () => {
  return useSelector((state: CombineState) => state.todo);
};
import React from "react";
import { ListGroup } from "reactstrap";
import { useTodoItems } from "../../hooks/todo";
import TaskItem from "./TaskItem";

const TaskList: React.FC = () => {
  const items = useTodoItems();
  return (
    <ListGroup className="mt-3">
      {items.map((item) => {
        return <TaskItem key={item.key} item={item} />;
      })}
    </ListGroup>
  );
};

export default TaskList;

また、外部ライブラリとして多くの便利Hooksが公開されていたりするので、Hooksのエコシステムを上手く活用して綺麗なComponentの記述を実現することもできそうです。

参考

さいごに

かなり長くなってきましたが、ここ半年で取り組んできたレガシーReact Projectに対する取り組みと、今後の改善点の一部を上げてみました。ここには長くなって書けませんが、テストまわりの取り組み(Cypress等)についてもどこかで書ければと思います。

それでは、よりよいReactライフを!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む