はじめに
この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux入門』の学習備忘録です。
前回の記事はこちら。
要約
- Reduxファイルは、
reducksパターン
で管理すると、開発・運用・保守全ての面で効率的になる -
selectors.js
にセレクター関数を定義することで、Store内のstateの値を、任意のコンポーネントで簡単に参照・取得できる。 - 外部API、DBとの通信時には、
redux-thunk
による非同期処理制御を入れる - Redux store からの state の取り出し方は「1.コンテナーコンポーネント」「2.
React-Hooks
」の二通りあるが、基本的には後者を採用するべき
#8...re-ducksパターンでファイル管理をしよう
re-ducksパターンとは?
Redux 関連ファイルのディレクトリ構成パターン。ファイルの分割基準をルールとして決めてしまうことで、ファイルを管理しやすくする、というもの。
ディレクトリ構成
reducks
├ users
├ products
⋮
state ごとにディレクトリを分けます。
ファイル構成
users
├ actions.js
├ index.js
├ operation.js
├ reducers.js
├ selectors.js
└ type.js
state の名前によらず、これら Redux 関連ファイルのファイル名は統一します。
各ファイルの役割をまとめます。
actions.js
Fluxフローにおける最初の窓口。アプリから受け取った state の変更依頼を受け取り、 reducers.js
に渡す。
operations.js
actions.js
の前に実行したい「何らかの複雑な処理」を書くための場所(例:外部APIやDBから値を取得する、等)
このファイルを用意することで、actions.js
やreducers.js
の記述をシンプルかつ画一的に保つことができるようになる。次回動画以降登場。
reducers.js
actions.js
からデータを受け取り、 Store の state をどう変更するか決める。
types.js
Typescript 使用時のみ作成する。型定義を記述して export する。
selectors.js
Store で管理している state を参照する関数を定義して export する。
「どこに何を書くべきか」を決めておくことで、開発スピードアップだけでなく、保守・運用時の手間も最小化できそうです。
selectors.jsの使い方
今回は、selectors.js
を作成して、state の参照を実行してみます。
import { createSelector } from "reselect";
const usersSelector = (state) => state.users;
export const getUserId = createSelector(
[usersSelector],
state => state.uid
)
getUserId
という関数を定義しています。これで、Store の中で管理されている state のうち、 users.uid を、任意のコンポーネントで参照・取得できます。
早速、 Home.jsx で使ってみます。
import React from 'react';
import {getUserId} from '../reducks/users/selectors';
import {useSelector} from 'react-redux'
const Home = () => {
const selector = useSelector(state => state);
const uid = getUserId(selector);
return (
<div>
<h2>Home</h2>
<p>{uid}</p>
</div>
);
};
export default Home
useSelector()
は React Hooks
の一種で、Store 全体の state を受け取ります。。これをgetUserId()
に渡すことで、 uid を取り出せます。
localhost:3000 をみてみると、
問題なく取得ができています(0000 は initialState.js
で定義した user.uid)
さらに、既に定義してある signInAction
を用いて、Store 内の state を更新し、getUserId()
で正しく参照できるかを試してみます。
import React from 'react';
import {useDispatch} from "react-redux";
import {push} from "connected-react-router";
import {signInAction} from "../reducks/users/actions"
const Login = () => {
const dispatch = useDispatch();
return (
<div>
<h2>ログイン</h2>
<button onClick={() => {
dispatch(signInAction({uid:"0001", username: "torahack"}))
dispatch(push('/'))}} >
ログイン
</button>
</div>
);
};
export default Login
<button>
をクリックすることでsignInAction
が発火し、state.user が更新されるはずです、。
http://localhost:3000/login より、ボタンをクリックすると、
↓
無事、state.user.uid が更新が確認できます!
#9...redux-thunkで非同期処理を制御すべし
非同期処理とは?
時間のかかる処理
と並行して、次の処理を進めてしまうこと。
時間のかかる処理
とは、例えば外部APIとのリクエスト・レスポンスの処理や、データベースとの通信などを指す。
通常の React では”時間のかかる処理”が完了する前に次の処理がどんどん進んでいきます(非同期で処理が進む)。しかし例えば「データベースへクエリを出し、返ってきた結果を Redux の Store に保存する」といった場面では、結果が返ってくるまでは処理を止めておかなければ、正常は画面描画を行えません。
redux-thunk とは?
React で非同期処理を制御するためのライブラリ。
actions.js
からreducers.js
へフローを渡すタイミングを制御できます。通常、redux-thunk の記述は、operations.js
に書くケースが多いです。
redux-thunk を導入
store.js
に、redux-thunk を導入します。
⋮
import thunk from "redux-thunk";
export default function createStore(history) {
⋮
applyMiddleware(
routerMiddleware(history),
thunk
)
)
}
たった2行追加するだけでOK。次に、operations.js
を追加します。
import { signInAction } from "./actions";
import { push } from "connected-react-router";
export const signIn = () => {
return async (dispatch, getState) => {
const state = getState()
const isSignedIn = state.users.isSignedIn
if(!isSignedIn) {
// 実際は以下にfirebaseと通信をするようなサインイン処理を書くが、
// まだ実装していないのでダミー処理を書く
const url = 'https://api.github.com/users/deatiger'
const response = await fetch(url)
.then(res => res.json())
.catch(() => null)
const username = response.login
dispatch(signInAction({
// state.user に対する変更内容
isSignedIn: true,
uid:"0002",
username: username,
}))
// 上記処理後、ルートへリダイレクト
dispatch(push('/'))
}
}
}
-
async
と書くことで、await
(その処理が完了するまで次の処理に進まない)を使える -
getState()
で store から state を取得できる。 -
dispatch()
で actions および push メソッドを使用できる。
if(!isSignedIn){...
以下は、本来であればバックエンド( fireabase など)との通信を行い、ユーザー認証の結果を action へ渡すことで、store 内の state の変更を行います。
今回はバックエンド側は未実装なので、ダミーとして ユーザー名deatiger
のgithub APIを叩く仕様にしています。正常に動けば、該当ユーザーのユーザー名を取得し、それを username に格納するよう、action へ命令を出します。
このoperations.js
を使用できるように、templates に変更を加えます。今回はLogin.jsx
内のログインボタンを押した時にoperations.js
が発火し、記述した通りの state の変更がなされるように記述します。
import React from 'react';
import {useDispatch} from "react-redux";
import {signIn} from "../reducks/users/operations"
const Login = () => {
const dispatch = useDispatch();
return (
<div>
<h2>ログイン</h2>
<button onClick={() => dispatch(signIn())} >
ログイン
</button>
</div>
);
};
export default Login
<button>
タグの onClick イベントとして 先ほどの operations.js
をセットしています。上手くいけば、 state.user の情報が更新されたのち、ルートへリダイレクトされるはずです。
↓ ログインボタンをクリックすると、
operations.js
の記述の通り、uid が 0002 へ変更されています!
ついでに、この画面(Home.jsx
)で username も表示させてみます。templates ファイルでStore内のstateを取得するためには、selectors.js
で、 username を取得する関数を定義する必要があります。
import { createSelector } from "reselect";
const usersSelector = (state) => state.users;
export const getUserId = createSelector(
[usersSelector],
state => state.uid
)
// 以下追記
export const getUserName = createSelector(
[usersSelector],
state => state.name
)
state.user.username を取得する関数として getUserName()
を定義します。
import React from 'react';
import {getUserId, getUserName} from '../reducks/users/selectors';
import {useSelector} from 'react-redux'
const Home = () => {
const selector = useSelector(state => state);
const uid = getUserId(selector);
const username = getUserName(selector);
return (
<div>
<h2>Home</h2>
<p>{uid}</p>
<p>ユーザー名: {username}</p>
</div>
);
};
export default Home
getUserName()
を import して使います。ブラウザで確認しましょう。
↓ localhost:3000/login でボタンを押すと、
username も変更されています!
もし今回の処理を redux-thunk による非同期処理制御を入れなかった場合、github api よりユーザー情報を取得する処理の完了を待たずに action の発行に進んでしまうため、ユーザー情報がうまく画面が表示されなくなってしまいます。
「外部APIやDBとの通信を行う際には非同期制御を入れる」と覚えておけば、大体のケースには対応できそうです。
#10...コンテナーの役割
コンテナーコンポーネントとは
Store とコンポーネントの中継役。 Redux(Store) の世界と React(アプリ) の世界をつなぐ。
かつては更新された Store 内 state を アプリに渡すために唯一の手段でしたが、現在はRedux-Hooks
を用いることでも Store 内 stateを渡せるようになりました。
基本的にはRedux-Hooks
の方が記述が少なくて楽なため、コンテナーコンポーネントを使う場面は限られています。
いつ使うべき?
明示的に state をフィルタリングしたいに使用します。
例えばユーザー認証情報など、セキュリティの関連から state を渡すコンポーネントを最小限に抑えたいときなどで、コンテナーコンポーネントがしばしば用いられます。
また、「React Hooks
が登場する以前に書かれた React+Reduxコードを理解するために、知識としては持っておくべき」という観点も、学習するモチベーションと言えます。
connect() の使い方
コネクトコンポーネントはsrc/containers/
の下に保存します。
今回は、React Hooks
で実装していた、Login Component への state の引き渡しを、コネクトコンポーネントで実装してみます。
実装ファイルは、以下の5ファイル。
1. src/containers/Login.js (コンテナーコンポーネント)
2. src/templates/LoginClass.jsx (クラスコンポーネントで実装したログインコンポーネント。コンテナーコンポーネントからの state を受け取るためには、関数コンポーネントではなく、クラスコンポーネントである必要がある)
3. src/containers/index.js
4. src/Route.jsx (/login の読み込み先を、LoginClassコンポーネントへ変更)
import LoginClass from '../templates/LoginClass'
import {compose} from 'redux'
import {connect} from 'react-redux';
import * as Actions from '../reducks/users/operations';
const mapStateToProps = state => {
return {
users: state.users // 渡したい state だけをオブジェクト型で記述
}
}
const mapDispatchToProps = dispatch => {
return {
actions: {
signIn() {
dispatch(Actions.signIn()) // Store から Dispatch する関数
}
}
}
}
export default compose(
connect(
mapStateToProps,
mapDispatchToProps
)
)(LoginClass)
コネクトコンポーネントで state を渡されるコンポーネントは、クラスコンポーネントがある必要があります。Login Component をクラスコンポーネントで定義し直したものを用意します。
import React, {Component} from 'react';
export default class LoginClass extends Component {
render() {
return (
<div>
<h2>ログイン</h2>
<button onClick={() => dispatch(signIn())} >
ログイン
</button>
</div>
)
}
}
import React, {Component} from 'react';
export default class LoginClass extends Component {
render() {
return (
<div>
<h2>ログイン</h2>
<button onClick={() => this.props.actions.signIn()} >
ログイン
</button>
</div>
)
}
}
export {default as LoginContainer } from './Login'
import React from 'react';
import {Route, Switch} from "react-router";
import {Login, Home} from "./templates";
import {LoginContainer} from "./containers"
const Router = () => {
return (
<Switch>
{/* <Route exact path={"/login"} component={Login} /> */}
<Route exact path={"/login"} component={LoginContainer} />
<Route exact path={"(/)?"} component={Home} />
</Switch>
);
};
export default Router
ここまで実装することで、#9の最後と同じブラウザ表示を確認することができるはずです。
コンテナーコンポーネントによる実装は、Redux Hooks
に比べ記述量が多く、かつファイル数も増えてしまいます。
知識としては持っておくべきですが、特別な事情がない限りはRedux Hooks
を使用すべきでしょう。
おわり
今回記事を要点をまとめると、
- Reduxファイルは、
reducksパターン
で管理すると、開発・運用・保守全ての面で効率的になる -
selectors.js
にセレクター関数を定義することで、Store内のstateの値を、任意のコンポーネントで簡単に参照・取得できる。 - 外部API、DBとの通信時には、
redux-thunk
による非同期処理制御を入れる - Redux store からの state の取り出し方は「1.コンテナーコンポーネント」「2.
React-Hooks
」の二通りあるが、基本的には後者を採用するべき
です。
今回はここまで!次回からは実践編として、実際にECアプリの開発を通じた学習が始まる予定です。