状態管理ライブラリを一言で言うと
ReactやVueでstateの伝言ゲームをなんとかするために生まれたもの
ざっくりいうとReduxの場合、みんなが共通の掲示板でstateを管理しよう、というふうに解決する。
なぜ状態管理ライブラリを使うのか
状態管理ライブラリを使うには、どうすればいい?というのは調べればわかるかもしれない。
しかし、状態管理ライブラリを使う目的、の方を理解するにはむしろ
状態管理ライブラリより、Reactに関する知識がまず必要である。
例えば状態管理ライブラリのできることとして次のようなことが挙げられる。
- グローバルステートの管理
- props drillingの緩和
- 不要なコンポーネントの更新をなくす
...などなど
これらのメリットは全てReactであったなんらかの課題に基づいているわけだが
Reactでの開発経験が薄いとイメージしづらい。
そもそも状態を管理したいのはなぜ
状態管理ライブラリはもちろん、状態管理に役立つ。
だがそもそも状態を管理したいときはどういうときなのか?
この記事はそれを調べる。
状態管理ライブラリは状態(を全体で)管理(する)ライブラリと思って欲しい。
そして、そもそもReactのstateは基本的にローカルだと考えて欲しい。
それを他と共有しようとすると、
以下のようなReactのトピックが関係する。
- コンポーネントの親子関係とは
- 再レンダリングとは?
- useStateで何ができる?
- useContextで何ができる?
- props drillingとは?
この記事は状態管理ライブラリを使う前に理解しておくべき
このような知識を整理する。
最後にReduxも少し紹介する。
コンポーネントの親子関係とは
ネストされている方が子供。ネストしている方が親。
これだけである。
propsは基本、この親から子にしか渡せない。
レンダリングとは
state, propsの変更があったときにReactがコンポーネントの更新を行うプロセスである。
そのプロセスをまとめると以下のようになる。
- まずReactが仮想DOMに新しい状態を反映する。
- その時古い方の仮想DOMと比較する。
- 変わったところだけ実際のDOMに反映する。
またReactのレンダリング = コンポーネントの更新と考えておけば良い。
備考
なお、DOMを読み込むのはブラウザである。
Reactによるコンポーネントの更新 = ブラウザによるDOM読み込みではない。
ただ過去の(英語)ドキュメントでその二つの呼び名が混じった時期があるらしい。
参考: 下の記事のコメント欄に、Reactのドキュメント翻訳者が出没してくれている。
(↓最近公式がブラウザのDOM読み込みを"paint"と呼ぶことにしたらしい)
useStateとは
stateが必要になる理由は二つある。
- ローカルの変数はレンダリング間で持続しない
- ローカルの変数はレンダリングのトリガーにならない
そこでこのようなものがあればUIを新しく更新できるよなーとなるわけである。
- レンダリングの間でデータを保持してくれる状態変数
- Reactに再度コンポーネントを読み込んでもらうようにする関数
そして、まさにこれらがuseStateの提供するものである。
コンポーネントの更新
コンポーネントのstateが変わるとき、そのコンポーネントは見た目が更新される。
また親コンポーネントが更新されるということは中にネストされている子コンポーネントも更新される。
参考: こちらの記事にあるように、コンポーネントの更新を防ぎたい場合にはパターンがある。
props drillingとは
propsを親から子にしか渡せないというのを覚えているだろうか。
子供は親がネストする必要がある。
つまり、「ネストしたコンポーネントに「ネストしたコンポーネントに「ネストしたコンポーネントに」」」propsを渡す、
というような場合が出てくる。
このように深く深くpropsが伝播してしまうことをprops drillingという。
useContextとは
props drillingの解決策で、
深くネストされたコンポーネントが離れた親コンポーネントからデータを取得する方法である。
コンテキストでの、データの取得は「ワープ」に例えられる。
(propsが順々に伝わっていくしかないのに対し)
使用の手順は以下の通り(詳しくはドキュメント参照)
- createContextを使う
- 親がそのProviderを用意する
- 深くネストされているコンポーネントがuseContextを使う
例えばこんなコードスニペットがある
https://playcode.io/react
↑にコピペして試せるコードにした。
import React, {useState} from 'react';
export function App() {
const [count, setCount] = useState(0)
return (
<div className='App'>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
このコードはシンプルこの上ない。
だがなぜシンプルなのかというと、ここにはローカルなstateしか存在していないからだ。
ローカルなステートが増えると、countが増える。
countというstateは、このコンポーネントから出ていくことはない。
他のコンポーネントでcountのstateを使うには?
まずあがるのは、子コンポーネントを作り、それにpropsとして渡す方法である。
しかしもし共有したいコンポーネントがたくさんあったら?
選択肢は他に二つある。
- Contextを使う
- Reduxなどの状態管理ライブラリを使う
ようやく状態管理ライブラリのメリット
ざっくりいうと、
状態管理ライブラリは
Reactの提供するuseContextと目的が似ているが
やることが増える + それなりのメリットもついてくる
という感じである。
Reduxを使うと、
コンポーネントの中からstateをどう動かすか、
という部分のコードが消える(別のファイルに分ける)。
先程のコードにReduxを使ってみる
ボタンを押すとcountが1増える・減る だけのアプリにわざわざreduxをつかうとどうなるのか?
App.js, action.js, reducer.jsに分かれる
まずインポート部分から
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './action';
./actionというファイルからincrement, decrementをインポートしているのに注目。
また、useSelector, useDispatchというuseなんたらというものがreact-reduxからインポートされている。
useSelector, useDispatchについて
この記事からそのまま説明をほぼ引用させてもらう。
- useSelector...stateに変更があったら自動的に再実行され、コンポーネントを再描画する
- useDispatchはdispatch関数を返す
- dispatch関数は、"action"を引数にとる。実行すると、stateが更新される
function App() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
// onClickのところでactionを使う
</div>
);
}
export default App;
actionを発するのに、dispatchという関数が使われるのがわかる。
actionの定義を見る
export const increment = () => {
return {
type: 'INCREMENT'
};
};
export const decrement = () => {
return {
type: 'DECREMENT'
};
};
type: 'INCREMENT'というわけのわからない指定がされている。
increment, decremetは本当に普通のJavaScript関数である。
INCREMENT, DECREMENTを使用するファイル
const initialState = {
count: 0
};
export const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
先程の指定はこのようなロジック分岐に使われている。
このようにして完全に別のファイルにステートの操作を分離できるのがReduxのメリットである。
第一引数state = initialStateという代入は、
stateが定義されていなかった場合にのみinitialStateを使用する。
ところでこのcounterReducer, 今までのコードで使われていないじゃないかと思った方
この記事では説明を省いています。
Reduxのついての記事をご覧ください。
とりあえずコピペして試したいあなたへ
以下のコードを同じディレクトリに入れ、index.htmlをブラウザにドラッグすると動く。
なおChatGPTに書いて貰ったもよう
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple React-Redux Example</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.4/react-redux.min.js"></script>
</head>
<body>
<div id="root"></div>
<script src="./app.js"></script>
</body>
</html>
// action.js
const increment = () => ({
type: 'INCREMENT',
});
const decrement = () => ({
type: 'DECREMENT',
});
// reducer.js
const initialState = {
count: 0,
};
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
// Store
const { createStore } = Redux;
const store = createStore(counterReducer);
// App.js
const App = () => {
const dispatch = ReactRedux.useDispatch();
const count = ReactRedux.useSelector((state) => state.count);
return React.createElement(
'div',
null,
React.createElement('h1', null, `Count: ${count}`),
React.createElement('button', { onClick: () => dispatch(increment()) }, 'Increment'),
React.createElement('button', { onClick: () => dispatch(decrement()) }, 'Decrement')
);
};
// index.js
const { Provider } = ReactRedux;
ReactDOM.render(
React.createElement(Provider, { store }, React.createElement(App)),
document.getElementById('root')
);