おはこんばんちは、@ちーずです。
アドベントカレンダー2日目はReactのステート管理についてです。
Reactでは、いろんな方法でステート管理できますね。
React hooksでは、useState
やuseReducer
、useContext
など...
さらにstate管理のライブラリも、Redux
やMobX
、recoil
など色々あって何がなんだかわからない...
そう感じている人もいると思います。(※ 自分です)
それらをどのように使うことができるか、どんな時に使うと良いかなどをまとめてみました!!
React hooksでのステート管理
1. useState
一番基本的なState管理ができるhooksです。
// const [state変数, stateの更新関数] = useState(初期値)
const [state, setState] = useState(initialState)
2. useReducer
useState同様、State管理ができるhooksです。
// reducer関数
// const reducer = (1つ前のState, action) => State
// actionは基本、type と payload を持っている
const reducer = (state, action) => {
switch (action.type) {
case "ACTION_NAME":
return { ...state, newState }
default:
return state;
}
}
// const [state変数, reducerの実行関数] = useReducer(reducer関数, 初期値)
const [state, dispatch] = useReducer(reducer, initialState)
useState
に比べて、少々複雑に感じますね。
シンプルなステート管理はuseState
で事足りるのですが、
下記のようなケースはuseReducer
の方が適していると言われています。
- 複数の値を操作する必要がある複雑なロジック
- 前のstateに基づいて次のstateを操作したい時
詳しくみていきましょう!
事例1. 複数の値を操作するuseReducer
今回は、名前と電話番号を入力するフォームを想定して実装してみます。
import { useReducer, Reducer } from "react";
// Stateの型
type State = {
name: string;
telNum: string;
};
// Actionの型
type Action = {
type: "UPDATE_NAME" | "UPDATE_TEL";
payload: string;
};
// reducer
const reducer: Reducer<State, Action> = (state, action) => {
switch (action.type) {
case "UPDATE_NAME":
return { ...state, name: action.payload };
case "UPDATE_TEL":
return { ...state, tel: action.payload };
default:
return state;
}
};
// 初期値の設定
const initialState = {
name: "",
telNum: ""
};
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<div>
<label>名前: </label>
<input
value={state.name}
// dispatchを使ってreducerにactionを送る
onChange={(e) => {
dispatch({
type: "UPDATE_NAME",
payload: e.target.value
});
}}
/>
</div>
...
</>
);
};
useState
で記述する場合、stateを別々で扱う必要がありますが、
useReducer
を使うことで1つのstateにすることができます。
また、useReducer
の重要な特徴として、reducerがstateに依存した関数ではないこと、
つまり非依存な純粋関数です。
そのため、テストの記述のしやすい、可読性があがる、などのメリットがあります。
事例2. useReducer
を使って記述を短くできるケース
useReducer
は前のstateに基づいて次のstateが決まる処理がお得意です。
そのため、toggle
を実現するには非常に相性が良いです。
// useStateの場合
const [isOpen, setIsOpen] = useState(false)
const toggleIsOpen = () => setIsOpen(!isOpen);
// useReducerの場合
const [isOpen, toggleIsOpen] = useReducer((prev) => !prev, false);
`useReducerを使うと1行でかけちゃいましたね!!
useReducer
を使ってパフォーマンス改善?!
useReducer
のdispatch関数
はメモ化されます。
そのため、React.memo
を組み合わせて使うことでパフォーマンスを最適化することができます。
▼ 参考
しかし、useState
, useReducer
はローカルステートであり、
Propsしない限り、単一のコンポーネントの中でしかstateを扱うことができません。
そこで登場するのがReact Context
です。
3. React Context API
React Context API
は、propsのバケツリレーをせずとも、
コンポーネントの階層に関係なくstateを子コンポーネントに伝えることができるAPIです。
import { createContext, useState, useContext } from 'react';
// 1. Contextの作成
// (Contextはstateを管理する箱、Storeのようなものと考えればok)
export const Context = createContext(undefined);
// 2. Context.Providerコンポーネントの作成
// Providerは子コンポーネントにstateを配布できるようにする役割
// 渡したいstateやstate更新関数をvalueに渡すことで、Contextに値を流し込むことができる
export const ContextProvider = ({ children }) => {
const [state, setState] = useState('');
return (
<Context.Provider value={[state, setState]}>
{children}
</Context.Provider>
);
};
// 3. Contextに格納された値をhooksで取得する
// Consumerと役割は同様
export const useStateContext = () => {
return useContext()
}
最初は書き方に慣れないかもしれませんが、
このようにとても簡単にグローバルなステート管理が実現できます。
また、useReducer
とReact Context
を組み合わせて使えば、
後述するReduxに近しいデータストア設計を作成することもできます。
しかし、React Context
はいくつかのデメリットがあります。
-
React Context
は複数作ることができるため、データフローの整備が難しい - ちゃんとメモ化しないと、再レンダリング地獄になる可能性あり...
- Providerの値が更新される度に、useContextを利用しているコンポーネントはすべて再レンダリングされるため
▼ 参考
ライブラリを利用したステート管理
個人的には基本的にReact Hooks
が提供してくれているState管理で十分かな、と思っていますが、
世の中にはState管理ができる様々なライブラリがあります。
ライブラリを用いいると、データストアを外部に持たせたり、
データフローのグラフ化やロギングなどよりリッチなState管理ができるものが多いです。
その中から特に人気のライブラリを軽くご紹介します!
1. Redux
まずはState管理ライブラリの王といっても過言ではない、Redux
をご紹介します。
Redux
はReact Context API
と異なりコンポーネントツリーの外部に1つのデータストアを持たせて
Stateの管理ができるライブラリです。
様々なメソッドや管理ツール(Redux Toolkit)が提供されており、
より柔軟にState管理ができます。
▼ 公式ドキュメント
▼ 参考
アプリケーション全体のStateを一つの場所で管理することができるため、データフローを整備しやすいですが、
一方で全体のStateをオブジェクトで管理しており、常に更新し続けるような設計になっています。
そのため、ちょっとしたコードのミスでStateが一気に消失してしまう可能性も...
そこで生まれたのがRecoil
です。
2. Recoil
Recoil
はコンポーネントツリーの外部にAtomというデータストアを複数持たせてStateの管理ができるライブラリです。
本家Facebookによって公開されたState管理ライブラリであり、今ちょっとアツめです。(※ 主観です)
(正直自分も使ったことがない、詳しくないため、別途調査して追記していきます...!)
▼ 公式ドキュメント
▼ 参考
結局どれを選べばいいの...?
正直本当にケースバイケースだな、という印象です。
記事を通して筆者が考えた使い分けは以下の通りです!
(まだ初学者よりであるため、ご意見いただけるとありがたいです!!)
- シンプルなローカルステート(プリミティブ型、一次元)を扱う →
useState
- 複雑なローカルステート(オブジェクト)を扱う →
useReducer
- ローカルステート & 前のstateに基づいて次のstateを操作したい時 →
useReducer
- グローバルなステート管理をしたい →
React Context API
- 大規模プロジェクトでデータフローを整備しながらグローバルなステート管理をしたい →
ライブラリを用いる
- 1つのデータストアで管理したい →
Redux
- 複数のデータストアで管理したい →
Recoil
- 1つのデータストアで管理したい →
また、様々な観点から比較してみた記事もたくさんあるので、
いろんな情報を参考にしてみて、サービスにあったState管理を探してみるのがよいのかな?と思いました!
以上、ReactのState管理についてでした!!
明日はuseEffectのお話をします