ReactでReduxを使わずにFluxパターンを実装する

## これはなに
新しめのReact (※) にはContextとHooksという2つのAPIが追加されており、これらを用いるとReduxで実装するようなFluxパターンを実現することが可能です。いつかくる最終戦争によって世界中のReduxが滅んでも、Reactさえ使えればクリーンな状態管理を続けることができるという訳ですね。

Reduxのサンプルでよくある絞り込み機能付きのToDoリストを用いて、storeを作ってblobal stateを共有しcomponentから利用するところまでを解説します。コードは[GitHub](https://github.com/zakioooo/flux-with-reducer-hook)にあります。

また、以下の本文中に添付するコードは可読性の都合で一部改変しているところがあります。ご了承ください。

※ - Context APIは v16.3.0、Hooks APIはv16.8.0 からです。

## 構成
`src` ディレクトリの中身はこんな感じです。

```
.
├── App.css
├── App.tsx
├── components
│   ├── TodoAdd
│   │   └── index.tsx
│   ├── TodoList
│   │   └── index.tsx
│   └── VisibilityFilterSelect
│       └── index.tsx
├── index.scss
├── index.tsx
├── react-app-env.d.ts
├── store
│   ├── index.tsx
│   ├── todos
│   │   ├── actions.ts
│   │   ├── index.tsx
│   │   ├── reducer.ts
│   │   └── types.ts
│   ├── visibilityFilter
│   │   ├── actions.ts
│   │   ├── index.tsx
│   │   ├── reducer.ts
│   │   └── types.ts
│   └── visibleTodos
│       └── index.tsx
└── types
    └── index.ts
```

ぱっと見た感じはReact-Reduxプロジェクトみのある構成ですね。

## storeを作る
### 型を定義する
TypeScriptを使う場合のみですが、storeの型を定義しておきます。
`todos` の型はこんな感じです。

[GitHub](https://github.com/zakioooo/flux-with-reducer-hook/blob/master/src/store/todos/types.ts)

```typescript:src/store/todos/types.ts
export type TodosState = {
  id: number;
  text: string;
  completed: boolean;
}[];

export type TodosAction = {
  type: "ADD_TODO" | "TOGGLE_TODO";
  id?: number;
  text?: string;
};

export type TodosContextType = React.Context<{
  state: TodosState;
  dispatch: React.Dispatch<TodosAction>;
}>;
```

`TodosState` と `TodosAction` はReduxでも使うので「あーアレね」という感じだと思います。 `TodosContextType` はReduxでいう `provider` の中身に相当するもので、一旦 `state` をそれを変更するための `dispatch` が入る…ということだけ定義しておきます。

### reducerを定義する

stateとactionの型が決まったので、Reducerを作っていきます。

[GitHub](https://github.com/zakioooo/flux-with-reducer-hook/blob/master/src/store/todos/reducer.ts)

```typescript:src/store/todos/reducer.ts
export default (
  state: TodosState,
  action: TodosAction
): TodosState => {
  switch (action.type) {
    case "ADD_TODO":
      return (action.text)
        ? [...state, {
          id: state.length,
          text: action.text,
          completed: false,
        }]
        : state;
    case "TOGGLE_TODO":
      return state.map(
        (item) => (item.id === action.id)
          ? { ...item, completed: !item.completed }
          : item
      );
    default:
      return state;
  }
};
```

まあ特に変なところはないですね。Reduxで使うReducerとなんら変わらないと思います。

### actionsを定義する

Reducerに変更を通達するactionsを実装します。

[GitHub](https://github.com/zakioooo/flux-with-reducer-hook/blob/master/src/store/todos/actions.ts)

```typescript:src/store/todos/actions.ts
export const addTodo = (text: string): TodosAction => ({
  type: "ADD_TODO",
  text,
})

export const toggleTodo = (id: number): TodosAction => ({
  type: "TOGGLE_TODO",
  id,
});
```

普通ですね。

### ContextProviderを定義する

やってきました。本丸です。
Storeを作成し、その状態と変更関数を子componentに配布していきます。

[GitHub](https://github.com/zakioooo/flux-with-reducer-hook/blob/master/src/store/todos/index.tsx)

```tsx:/store/todos/index.tsx
const initialState: TodosState = [];

export const TodosContext: TodosContextType = React.createContext<{
  state: TodosState;
  dispatch: React.Dispatch<TodosAction> | Noop;
}>({
  state: initialState,
  dispatch: () => {},
});

const TodosContextProvider: React.FC<React.Props<{}>> = ({
  children,
}): JSX.Element => {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  return (
    <TodosContext.Provider value={{ state, dispatch }}>
      {children}
    </TodosContext.Provider>
  );
};

export default TodosContextProvider;
```

なげえ。一気にややこしくなりました。
順番に解説します。

まずは `Context API` を使って子componentに渡すための `Context` を実装しています。
ちなみに引数に渡す値は初期値ではなく **子componentが読み込みを試みたとき該当するProviderが無かった際に使われる値** です。実質ほぼ使われないのですが、適当にnullとか入れると型解析の都合が悪くなることなどもあり、いちおう初期値いれとくか…というぐらいの気持ちに落ち着きました。

子componentから呼び出す際にも使用するので `export` を忘れないように気をつけます。

```tsx:/store/todos/index.tsx
const initialState: TodosState = [];

export const TodosContext: TodosContextType = React.createContext<{
  state: TodosState;
  dispatch: React.Dispatch<TodosAction> | Noop;
}>({
  state: initialState,
  dispatch: () => {},
});
```

で、いま作った `Context` に渡すReducerを作ります。

`useReducer` はreducerと初期値を受け取って `state` と `dispatch` を返します。Hooksを利用したことでこれらはこのcomponentのstateとして管理されるため、Context APIをよく考えずに使うと起きる「状態更新の度に再描画が走る」リスクは心配無用だと思います。

また、Context Providerは1つのstoreにつき1つ定義することになるのでどうやってもネスト地獄が発生します。変に結合させるとややこしくなるので `children` を受け取る形で実装しておき、後でまとめるとよいかと思います。

```tsx:/store/todos/index.tsx
const TodosContextProvider: React.FC<React.Props<{}>> = ({
  children,
}): JSX.Element => {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  return (
    <TodosContext.Provider value={{ state, dispatch }}>
      {children}
    </TodosContext.Provider>
  );
};

export default TodosContextProvider;
```

## storeを組み込む
いよいよアプリケーションにstoreを組み込んでゆきます。

[GitHub](https://github.com/zakioooo/flux-with-reducer-hook/blob/master/src/store/index.tsx)

```tsx:src/store/index.tsx
const ContextProvider: React.FC<React.Props<{}>> = ({
  children,
}): JSX.Element => (
  <VisibilityFilterContextProvider>
    <TodosContextProvider>
      <VisibleTodosContextProvider>
        {children}
      </VisibleTodosContextProvider>
    </TodosContextProvider>
  </VisibilityFilterContextProvider>
);

export default ContextProvider;
```

ネスト地獄です。こればかりは避けられませんが、逆にこういうAPI都合の謎構造は一箇所に固めて (ある程度) ブラックボックス化させておけるので、割り切ることもできるような気がしています。

特に言うことはないですが、後で解説するように「複数storeのstateを集約する」みたいなことをやろうとすると、ネストの順番が割と大事になってくるので注意が必要です。

最後にこれらをアプリケーションに接続します。 `App.tsx` 的な、ルート階層に近い部分で行います。このサンプルだと `src` 直下ですね。

[GitHub](https://raw.githubusercontent.com/zakioooo/flux-with-reducer-hook/master/src/index.tsx)

```tsx:src/index.tsx
const App: React.FC = (): JSX.Element => (
  <ContextProvider>
    <div className="container">
      <TodoAdd />
      <VisibilityFilterSelect />
      <TodoList />
    </div>
  </ContextProvider>
);

ReactDOM.render(<App />, document.getElementById('root'));
```

これでアプリケーションとstoreがつながりました。
いよいよ状態の読み出しと更新を行なっていきます。

## storeを使う
ToDoリストへの項目追加を行う `TodoAdd` コンポーネントを例に解説します。

[GitHub](https://github.com/zakioooo/flux-with-reducer-hook/blob/master/src/components/TodoAdd/index.tsx)

```tsx:src/components/TodoAdd/index.tsx
const TodoAdd: React.FC = (): JSX.Element => {
  const { state, dispatch } = React.useContext(TodosContext);
  const [ text, setText ] = React.useState("");
  const handleClick = () => {
    if (!dispatch) return;
    dispatch(addTodo(text));
    setText("");
  }
  return (
    <form className="TodoAdd">
      <input
        type="text"
        value={text}
        onChange={e => setText(e.currentTarget.value)}
        className="TodoAdd__input"
      />
      <button onClick={handleClick} disabled={text === ""} className="TodoAdd__button">追加</button>
    </form>
  )
}

export default TodoAdd;
```

大事なのは `useContext` を利用してContextから値を取り出し、そのまま利用できるという点ですね。

```tsx:src/components/TodoAdd/index.tsx
// ↓ここでContextを読み込んで
import { TodosContext } from "../../store/todos";

const TodoAdd: React.FC = (): JSX.Element => {
  // ↓ ここで値を取り出している！
  const { state, dispatch } = React.useContext(TodosContext);
  const [ text, setText ] = React.useState("");
  const handleClick = () => {
    if (!dispatch) return;
    // ↓ アクションをdispatchして状態を更新できる！
    dispatch(addTodo(text));
    setText("");
  }
  // ...
```

いいですね〜Reduxの `connect` に比べてシンプルな印象がないでしょうか。そんなことないですか。

## おまけ
### combinated storeを作る
2つ以上のstoreを結合したいときには単純にcontextをたくさん読み込んで計算した結果を新たなcontextとして配布すればよいのではないかと思います。

以下は `visibilityFilter` (表示するToDo項目の絞り込み) と `todos` (ToDo全項目) を使って「表示するToDo項目」を作っているものです。

[GitHub](https://github.com/zakioooo/flux-with-reducer-hook/blob/master/src/store/visibleTodos/index.tsx)

```tsx:src/store/visibleTodos/index.tsx
export const visibleTodosContext = React.createContext<{
  state: TodosState;
}>({
  state: initialState,
});

const VisibleTodosContextProvider: React.FC<React.Props<{}>> = ({
  children,
}): JSX.Element => {
  const { state: todosState } = React.useContext(TodosContext);
  const { state: visibilityFilterState } = React.useContext(VisibilityFilterContext);
  const stateFactory = (todos: TodosState): TodosState => {
    switch(visibilityFilterState) {
      case "SHOW_ALL":
        return todos;
      case "SHOW_ACTIVE":
        return todos.filter(todo => !todo.completed)
      case "SHOW_COMPLETED":
        return todos.filter(todo => todo.completed)
      default:
        return todos;
    }
  }
  
  return (
    <visibleTodosContext.Provider value={{ state: stateFactory(todosState) }}>
      {children}
    </visibleTodosContext.Provider>
  )
}

export default VisibleTodosContextProvider;
```
前述しましたが、 **Context APIのProviderは子componentにしか状態を伝播させない** ので、これを `ContextProvider` のルート階層に持っていくとエラーになります。
ちょっと気持ち悪さがありますが、やむを得ない感じもあります。

### 課題など
今回は使いませんでしたが、ほとんどのアプリケーションではAPIへのアクセスなど非同期処理が発生すると思います。今の所Reducer Hookにはmiddlewareの利用にデファクト的なものがないっぽいので、どうハンドリングするかは少し考える必要があります。同期的に処理の開始と完了で2つactionを発行するという手もあるし、[自前でmiddleware的なものを実装する](https://medium.com/front-end-weekly/writing-redux-like-simple-middleware-for-react-hooks-b163724a7058) という手もあるでしょう。


## まとめ
これでRedux封じに遭っても安心ですね。
今はまだ「使えんこともない」という感じですが、こうしたReact本来のAPIを活かしつつ拡張していくようなエコシステムが成熟してくると大分可能性はあるな…と感じます。以上です。