この記事は
React + Redux を使ったアプリケーションのデータの流れについて解説しています。
誰に向けた記事か
React + Redux をまだ使用したことがない or 経験がまだ少ない人。
React + Redux アプリのデータの流れ
React のコンポーネント
コンポーネントのコードは以下のようになっています。
const ProductList = () => (
<div>
<h1>商品リスト</li>
<ul>
<li>Nintendo Switch</li>
<li>PlayStation</li>
<li>Xbox</li>
</ul>
</div>
);
イメージにするとこのような木構造になっているとお考えください。
state と props
コンポーネントで扱うデータには state と props があります。
state はコンポーネント内部に持っているデータで、props は親から子に渡されるデータです。
以下の例では title
が state で、listTitle
が props になります。
// これが親コンポーネント
const Product = () => {
// これがstate
const [title, setTitle] = useState("商品リスト");
// HTMLの属性のようにして渡しているのがprops
return <ProductList listTitle={title} />;
};
// これが子コンポーネント
const ProductList = (props) => (
<div>
<h1>{props.listTitle}</li>
<ul>
<li>Nintendo Switch</li>
<li>PlayStation</li>
<li>Xbox</li>
</ul>
</div>
);
props にはデータだけでなくコールバック関数を渡すことができます。
この関数をイベントハンドラとして onClick などに設定することで、親のコンポーネントに value を渡すことができます。
以下の例では子コンポーネントにコールバック関数を渡し、親の state を更新しています。
// これが親コンポーネント
const Product = () => {
// これがstate、setTitleはtitleを変更するための関数
const [title, setTitle] = useState("商品リスト");
// HTMLの属性のようにして渡しているのがprops
// handleClickのようにコールバック関数を渡すこともできる
return <ProductList listTitle={title} handleClick={setTitle} />;
};
// これが子コンポーネント
const ProductList = (props) => (
<div>
<h1>{props.listTitle}</li>
<ul>
<li>Nintendo Switch</li>
<li>PlayStation</li>
<li>Xbox</li>
</ul>
<button onClick={() => props.handleClick("ゲーム機")}>タイトル変更</button>
</div>
);
render とライフサイクル関数
props が更新されると、それを受け取ったコンポーネントのツリーが自動的に更新されます。
具体的にはコンポーネントの render 関数やライフサイクル関数が再実行されます。
ライフサイクルに関して、公式サイトからリンクされていた図があるので、ご参照ください。
横断的関心事
別々のコンポーネントで同じデータを使いたい場合があったとします。
その場合は、共通の親コンポーネントから props をリレーのように渡す必要があります。
しかしこれはあまりスマートではないですよね?
Redux を使うとこの問題に対処できます。
Redux では Store と呼ばれる共有データストアから、直接コンポーネントに props を渡すことができます。
Redux の仕組み
Store からデータを props に渡すために、container と呼ばれるコンポーネントでラップします。
container コンポーネントでは props のデータに Store の一部のデータ、コールバック関数に dispatch と呼ばれる関数をマッピングします。
connect という関数を使って Store のデータと dispatch をマッピングします。
// Storeのどのデータをpropsにマッピングするか
const mapStateToProps = (state) => ({
isLogin: state.Auth.isLogin,
});
// LoginPageコンポーネントのpropsには、isLoginとdispatch関数が渡される
export default connect(mapStateToProps)(LoginPage);
Action
Store のデータを更新したい場合は、コンポーネントから dispatch 関数に対して Action と呼ばれる JavaScript オブジェクトを渡します。
以下のように、コンポーネントの onClick などから実行します。
ですが、この実装方法だと、type を間違えるなどのミスの原因になりそうですね。
const Button = (props) => (
<button onClick={() => props.dispatch({ type: "login" })}>ログイン</button>
);
そのため、通常は ActionCreator と呼ばれる関数を定義して共用します。
// Action Type
export const LOGIN = "Auth/LOGIN";
// Action Creator
export function login() {
return { type: LOGIN };
}
const Button = (props) => (
<button onClick={() => props.dispatch(login())}>ログイン</button>
);
実際には container コンポーネントでマッピングして利用することが多いです。
connect 関数の第 2 引数に、マッピングする関数を指定します。
// Storeのどのデータをpropsにマッピングするか
const mapStateToProps = (state) => ({
isLogin: state.Auth.isLogin,
});
// dispatchにどのActionCreatorをマッピングするか
const mapDispatchToProps = (dispatch) => ({
login: bindActionCreators(login, dispatch),
logout: bindActionCreators(logout, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(LoginPage);
こうすると、コンポーネントからはマッピングされた関数を実行するのみです。
const Button = (props) => (
<button onClick={() => props.login()}>ログイン</button>
);
Reducer
Action オブジェクトが dispatch 関数に渡されると、Reducer という関数が実行されます。
この Reducer が実行されると、Store のデータが更新されます。
Reducer は現在の Store と Action を受け取り、次の Store を返す関数です。
Action の type によって、return する state が異なるのがわかるかと思います。
※スプレッド演算子(...state)を使っているのは、引数で渡された state をコピーするためです。
Redux は state の更新を検知するために、浅い比較(shallow equal)を行います。
state を直接変更すると更新を検知できず、コンポーネントが再レンダリングされなくなります。
// 初期State
const initState = {
isLogin: false;
};
// Reducer関数
function reducer(state = initState, action) {
switch (action.type) {
case LOGIN:
return { ...state, isLogin: true };
case LOGOUT:
return { ...state, isLogin: false };
default:
return state;
}
}
Reducer が実行され Store が変更されると、Store の情報はマッピングされたコンポーネントに渡されます。
props が更新されると、コンポーネントのツリーが自動的に更新されるので、Store が更新されると View が自動的に更新されるような挙動になります。
Redux thunk
Store データの更新には dispatch 関数に Action オブジェクトを渡す、というのはわかったのですが、Action は type というキーが必須のただのオブジェクトです。
とてもシンプルですが、API 通信などの外部通信は、どこに記述するのが良いでしょうか?
// Action Type
export const LOGIN = "Auth/LOGIN";
// Action Creator
export function login() {
return { type: LOGIN };
}
通常 Action Creator は純粋な Action オブジェクトしか返しません。
Redux thunk を使うと、Action Creator が関数を返せるようになります。
// Action Type
export const LOGIN = "Auth/LOGIN";
// Action Creator
export function login() {
// dispatchを引数にとる関数を返す
return async (dispatch) => {
// API通信をawaitで待つ
await fetch("/auth/login");
// Actionをdispatch
dispatch({ type: LOGIN });
};
}
例えば API 通信の最中にアプリをローディング表示したい場合などは、以下のように通信前後で別々の Action を dispatch 関数に渡し、状態を変更します。
// Action Type
export const FETCHING = 'Auth/FETCHING';
export const LOGIN = 'Auth/LOGIN';
// 初期State
const initState = {
isFetching: false;
isLogin: false;
};
// Reducer関数
function reducer(state = initState, action) {
switch (action.type) {
case FETCHING:
return { ...state, isFetching: true };
case LOGIN:
return { ...state, isFetching: false, isLogin: true };
case LOGOUT:
return { ...state, isLogin: false };
default:
return state;
}
}
// Action Creator
export function login() {
// dispatchを引数にとる関数を返す
return async (dispatch) => {
// API通信前に別のActionをdispatch
dispatch({ type: FETCHING });
// API通信をawaitで待つ
await fetch('/auth/login');
// Actionをdispatch
dispatch({ type: LOGIN });
};
}
これでコンポーネント側で API 通信中の表示を制御できます。
// ログインページのコンポーネント
const LoginPage = (props) => {
// API通信中はローディングを表示
if (props.isFetching) {
return <Loading />;
}
return (
<>
<p>{props.isLogin ? "ログイン中" : "未ログイン"}</p>
<Button />
</>
);
};
// ログインボタン
const Button = (props) => (
<button onClick={() => props.login()}>ログイン</button>
);
最後に
以上、React + Redux アプリのデータの流れでした。
各ライブラリの挙動について、理解の助けになればと思います。