なるべくRedux使いたくないならHooksを学ぼう

# はじめに

Reactプロジェクトでのステート管理と言えばReduxを思い浮かべる方が多いのではないでしょうか。

Reduxは本当に素晴らしいライブラリであると思いますが、覚えるべきボイラープレートが多く、
元々シンプルだったReactがどんどん複雑になっているように感じることもあります。

Reactのバージョンアップによって追加されたhooksを利用することで、flux的なステート管理をReduxなしで実装することができます。


## 参考

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Most projects don’t need Redux if they use React Context correctly<br><br>So many problems b/c people try to put all their state in one context<br><br>If you break them up by concern instead, use useReducer, separate get/set contexts as needed, useMemo as needed -&gt; React Context is golden</p>&mdash; Andrew Jones (@ajones_codes) <a href="https://twitter.com/ajones_codes/status/1252609786635587589?ref_src=twsrc%5Etfw">April 21, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


> React コンテキストを正しく使えば、ほとんどのプロジェクトで Redux は必要ありません。

> 関心を分割し、必要に応じて useReducer を使い、必要に応じて get/set コンテキストを分け、必要に応じて useMemo を使う -> Reactは最高。


今回は、以下の記事を参考にしながらhooksを利用したflux的なステート管理について学びます。
https://dev.to/ankitjena/ciao-redux-using-react-hooks-and-context-effectively-398j



# 従来のステート管理

かつてはコンストラクタ内に記述していたステートは、hooksの登場によって以下のように宣言し使うことができるようになりました。

## `useState`

`useState`の使い方はとてもシンプルで、ステートとそのステートを更新するための関数を作成します。


```react
import { useState } from 'react'

const [name, setName] = useState("")
```


## ログインコンポーネントを実装する

ここで、`useState`を用いて簡素なログインのためのコンポーネントを記述してみます。
最終的には、flux的なステート管理を備えたログイン機能に育て上げる予定ですが、今はまだシンプルなステート管理のみとなっています。

```react
import React, { useState } from 'react';
import axios from 'axios';
import LoadingButton from '../component/LoadingButton';

export default function Login() {
  const url = 'https://www.example.com';
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [emailError, setEmailError] = useState(false);
  const [passwordError, setPasswordError] = useState(false);
  const [isLoading, setIsLoading] = useState(false);

  async function handleSubmit() {
    setIsLoading(true);
    const res = await axios.post(url, { email, password });
    if (res.data.status === 'EMAIL_ERROR') {
      setEmailError(true);
    }
    if (res.data.status === 'PASSWORD_ERROR') {
      setPasswordError(true);
    }
    // イイ感じの処理
  }

  return (
    <div>
      <h1>ログイン</h1>
      <div>
        <input
          type="text"
          value={email}
          onChange={
            (e) => setEmail(e.target.value)
          }
        />
        <input
          type="password"
          value={password}
          onChange={
            (e) => setPassword(e.target.value)
          }
        />
        { isLoading
          ? <LoadingButton />
          : <button type="button" onClick={() => handleSubmit()}>Sign in</button> }
      </div>
    </div>
  );
}
```

これはとてもシンプルな実装ですが、複雑なステート管理や、複数のコンポーネントを跨いだステート管理には不向きです。


# `useReducer`を使う

Reduxを使ったことがある方なら、useReducerの使い方には馴染みがあるかもしれません。
`useReducer`は引数を2つとり、新たなステートを返す関数です。

- 第1引数：ステート
- 第2引数：ステートを変化させるアクション

`useReducer`を使用することで、アクション単位でのステート更新を実現することができます。
今回は使っていませんが、例えば`LOGIN`というアクションに`username``password``isLoggedIn``isError`というステートを紐付けておけば、いちいちステートを一つづつ管理することなく、アクション単位でUIの状態を制御することができます。


## ログインコンポーネントに`useReducer`を追加する

ステートを`useReducer`によって管理するよう書き換えたコードが以下です。
実行した際の挙動は、先のコードと変わりありません。

先にステートの初期値（`initialState`）と`reducer`を定義し、
`userReducer`に与えることで、`State`と`dispatch`を得ることができます。

`state`を変更する際は`dispatch`関数を使います。


```react
import React, { useReducer } from 'react';
import axios from 'axios';
import LoadingButton from '../component/LoadingButton';

const url = 'https://www.example.com';

const initialState = {
  user: {
    email: '',
    password: '',
  },
  errors: {
    email: false,
    password: false,
  },
  isLoading: false,
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'CHANGE_VALUE':
      return {
        ...state,
        user: {
          ...state.user,
          [action.field]: action.data,
        },
      };
    case 'ERROR':
      return {
        ...state,
        errors: {
          ...state.errors,
          [action.field]: true,
        },
      };
    case 'LOADING':
      return {
        ...state,
        isLoading: true,
      };
    default: return state;
  }
};

export default function Login() {
  const [state, dispatch] = useReducer(reducer, initialState);

  async function handleSubmit() {
    dispatch({ type: 'LOADING' });
    const res = await axios.post(url, state.user);
    if (res.data.status === 'EMAIL_ERROR') {
      dispatch({ type: 'ERROR', field: 'email' });
    }
    if (res.data.status === 'PASSWORD_ERROR') {
      dispatch({ type: 'ERROR', field: 'password' });
    }
  }

  return (
    <div>
      <h1>ログイン</h1>
      <div>
        <input
          type="text"
          value={state.user.email}
          onChange={
            (e) => dispatch({ type: 'CHANGE_VALUE', data: e.target.value, field: 'email' })
          }
        />
        <input
          type="password"
          value={state.user.password}
          onChange={
            (e) => dispatch({ type: 'CHANGE_VALUE', data: e.target.value, field: 'password' })
          }
        />
        { state.isLoading
          ? <LoadingButton />
          : <button type="button" onClick={() => handleSubmit()}>Sign in</button> }
      </div>
    </div>
  );
}
```


個別の関数を扱うのではなく、1つの`reducer`を宣言し、いくつかのアクションとそれに対応するストアの変更を定義します。`useState` を使用している間は、要件が大きくなると変数の数を見失うことがあるので、これは非常に便利です。


# Viewとロジックを分離する

Reactの基本は、関心の分離です。
つまり、ロジックを担当するコンポーネントと、見た目を担当するコンポーネントを分けたいということです。
こうすることで、全体の見通しをよくしてバグを減らすだけでなく、コードの再利用性も高まります。



# `Custom Hooks`を使う

では`useLoginReducer.js`というファイルに、`reducer`部分を分割します。

```react:hooks/useLoginReducer.js
import { useReducer } from 'react';

const initialState = {
  user: {
  // ...省略
};

const reducer = (state, action) => {
  switch (action.type) {
    // ...省略
};

export default function useLoginReducer() {
  const [store, dispatch] = useReducer(reducer, initialState);
  return [store, dispatch];
}
```


ログインコンポーネントで、インポートして使うことができるようになりました！


```react:Login.js
import React from 'react';
import axios from 'axios';
import useLoginReducer from '../hooks/useLoginReducer';

export default function Login() {
  const [state, dispatch] = useLoginReducer();

  async function handleSubmit() {
    // ...省略
}
```


# Globalなステートを扱う

Redux(flux)を扱う1番のメリットは、ステートをグローバルに管理し、どのコンポーネントからもアクセスできるようになる点です。

`Hooks`を利用する場合は、`Context`を使用することで簡単にステートにアクセスすることができます。

`Context`では、

- データを保存または初期化する`Provider`と、
- データを読み込んだり更新したりする`Consumer`を宣言し利用します。


まずは先ほどの``LoginReducer`を`Context`に統合します。

```react:context.js
import React, { useReducer, useMemo } from 'react';

const globalContext = React.createContext();

const initialState = {
    // ...省略
};

const reducer = (state, action) => {
    // ...省略
};

export const StoreProvider = ({ children }) => {
  const [store, dispatch] = useReducer(reducer, initialState);

  // Storeが変化した時だけリレンダリングするようuseMemoを使用
  const contextValue = useMemo(() => [store, dispatch],
    [store, dispatch]
  );

  return (
    <globalContext.Provider value={contextValue}>
      { children }
    </globalContext.Provider>
  );
};

export function useStore() {
  return React.useContext(globalContext);
}
```

`App.js`で、コンポーネントを`Provider`で囲うと、`store``dispatch`を使用することができます。

```react:App.js
import React from 'react';
import './App.css';
import Login from './container/login';
import { StoreProvider } from './hooks/context';

function App() {
  return (
    <StoreProvider>
      <Login />
    </StoreProvider>
  );
}

export default App;
```


# さらに安全に、便利に使う

ここからが真骨頂ともいうべき特徴です。

## Stateを更新する／更新しないコンポーネントで関心を分ける

ステートを更新しないコンポーネントに`dispatch`を与える必要はありませんし、ステートを参照しないコンポーネントに`store`を与える必要もありません。

この2つを分けるのは、簡単です。

`dispatch`用のコンテクストと`store`用のコンテクストを分けて作り、それぞれ`export`して使用すればよいのです。


```react:context.js
import React, { useReducer, useMemo } from 'react';

const storeContext = React.createContext();
const dispatchContext = React.createContext();

// ...省略

export const StoreProvider = ({ children }) => {
  const [store, dispatch] = useReducer(reducer, initialState);
  // Storeが変化した時だけリレンダリングするようuseMemoを使用
  const contextValue = useMemo(() => [store, dispatch],
    [store, dispatch],
  );

  return (
    <dispatchContext.Provider value={contextValue[1]}>
      <storeContext.Provider value={contextValue[0]}>
        { children }
      </storeContext.Provider>
    </dispatchContext.Provider>
  );
};

export function useStore() {
  return React.useContext(storeContext);
}
export function useDispatch() {
  return React.useContext(dispatchContext);
}
```

それぞれを別々にインポートして使用できるようになりました。

```react:Login.js
import React from 'react';
import axios from 'axios';
import { useStore, useDispatch } from '../hooks/context';
import LoadingButton from '../component/LoadingButton';

export default function Login() {
  const state = useStore();
  const dispatch = useDispatch();

// ...省略
```


今回のログインコンポーネントでは、`store`も`dispatch`も両方使っていますが、どちらか片方のみ必要な場合は必要な物だけインポートして使用することが可能です。


## 複数の`Store`を扱う

全てのステートをたった一つのストアで管理するのは現実的ではありません。

複数のStoreに分けて管理しやすくすることが必要でしょう。


これを実現するために、`initialState` と `reducer` を引数にとり新しい`store`を返す関数を実装します。


```react:makeContexts.js
import React, { createContext, useContext, useReducer } from 'react';

export default function makeStore(reducer, initialState) {
  const storeContext = createContext();
  const dispatchContext = createContext();

  const StoreProvider = ({ children }) => {
    const [store, dispatch] = useReducer(reducer, initialState);

    return (
      <dispatchContext.Provider value={dispatch}>
        <storeContext.Provider value={store}>
          {children}
        </storeContext.Provider>
      </dispatchContext.Provider>
    );
  };

  function useStore() {
    return useContext(storeContext);
  }
  function useDispatch() {
    return useContext(dispatchContext);
  }

  return [StoreProvider, useStore, useDispatch];
}
```

あとは、必要に応じて**この関数を元に**`Store`（つまりプロバイダー、ストアの参照関数、ストアの更新関数）を作成します。

```react:loginContext.js
import makeStore from './makeStore';

const initialState = {
  // ...省略
};

const reducer = (state, action) => {
  // ...省略
};

const [
  LoginProvider,
  useLoginStore,
  useLoginDispatch,
] = makeStore(reducer, initialState);

export { LoginProvider, useLoginStore, useLoginDispatch };
```

Storeを必要としているコンポーネントを`Provider`で囲って使用します。


```react:App.js
import React from 'react';
import './App.css';
import Login from './container/login';
import { LoginProvider } from './hooks/loginContexts';

function App() {
  return (
    <LoginProvider>
      <Login />
    </LoginProvider>
  );
}

export default App;
```

囲まれたコンポーネントで、ストアを参照したり更新したりします。

```react:Login.js
import React from 'react';
import axios from 'axios';
import { useLoginStore, useLoginDispatch } from '../hooks/loginContexts';
import LoadingButton from '../component/LoadingButton';

export default function Login() {
  const state = useLoginStore();
  const dispatch = useLoginDispatch();

  // ...省略
```

あとは必要に応じて、ストアを自由に作成することができます。

例えば以下のような構成で使うこともでき、どのコンポーネントがどのストアを参照しているか一覧できます。

```react
function App() {
  return (
    <UserProvider>
        <Navbar />
        <ProductProvider>
            <Products />
        </ProductProvider>
    </UserProvider>
  );
}
```


# まとめ

React標準のHooksAPIを利用することで、Reduxを使わずにステート管理を実装することができました。

ほとんどの場合、Hooksで十分なのでは？と思います。