Reduxの学習メモです
Fluxフローについて
まずはReduxにおけるFluxフローの全体像を理解してみます
なぜReduxを使うのか
- stateの見通しをよくするため
- どこからでもstateを参照/変更可能にするため
- モジュールを疎結合にするため
=機能Aと機能Bがお互いに影響しあわない
ReactとReduxのstate管理の違い
Reactのみでstateを管理する場合
複数コンポーネントで利用しているstateに変更が発生した場合、
コンポーネントの親子関係を辿って変更を共有しないといけない。(stateのバケツリレー)
→親子関係が深く複雑になるほど管理が大変になる。
Reduxを使ってstateを管理する場合
複数コンポーネントで利用するstateをStoreに持たせることで、Storeに直接、変更をリクエストしたり、バケツリレーなしで関係するコンポーネントに変更を反映することができる。
Fluxフローとは
- データフロー設計の1つ
- データが常に1方向に流れる
- イベントによってデータが変化(イベント駆動)
Flux思想をReactの状態管理に適用したライブラリ
= Redux
Fluxフロー図
Actionsを書いてstateの変更を依頼する
Actionsの役割
アプリからStoreへデータを送るためのpayload(データの塊)を渡す役割
→アプリから受け取ったデータをReducersへ渡す
なぜActionsを使うのか
純粋にデータを渡す処理だけを記述するため
どのstateをどのように変更するのかについてはReducersに任せる
Actionsの書き方
ユーザーがサインインした時にStoreで管理しているユーザー情報の変更依頼を投げるAction
// Action typeを定義してexport
export const SIGN_IN = "SIGN_IN";
// Actionsはプレーンなobjectを返す
export const signInAction = (userState) => {
// typeとpayloadを記述する
return {
type: "SIGN_IN",
payload: {
isSignedIn: true,
uid: userState.uid,
username: userState.username
}
}
};
Reducersの作り方とスプレッド構文の使い方
Reducersはstateの変更を管理する役割を持っている
Reducersの役割
Actionsからデータを受け取り
Storeのstateをどう変更するのかを決める
→Store内のstate(状態)の管理人
initialStateを作る
const initialState = {
users: {
isSignedIn: false,
uid: "",
username: ""
}
};
export default initialState
- Storeの初期状態(アプリケーション起動時)
- アプリに必要なstateを全て記述
- exportしておく
Reducersを作る
import * as Actionss from './actions';
import initialState from "../store/initialState";
- actionsファイル内のモジュールを全てimport(Actionsという名前をつける)
- initialStateをimport
export const UserReducer= (state = initialState.users, action) => {
switch (action.type) {
case Actions.SIGN_IN:
return {
...state,
...action.payload
};
default:
return state
}
};
スプレッド構文について
スプレッド構文は配列やオブジェクトの要素を展開する
spread:広げる、開く、塗る
const payload = {
uid: "1",
username: "hoge"
}
console.log({...payload});
// {uid: "1", username: "hoge"}
// Merge Objects
const state = {
isSignedIn: false,
uid: "0",
username: "fuga"
}
console.log({...state, ...payload});
// {isSignedIn: false, uid: "1", username: "hoge"}
Redux(Store)とReactを接続してstateを変更する
Storeはstateを保存する
- StoreとReducersを関連づける
- Redux(Store)とReactを接続する
- Storeの状態を変更する
Store | モジュールのimport
// reduxモジュールのimport
import {
createStore as reduxCreateStore,
combineReducers
} from 'redux';
// Reducersのimport
import { ProductsReducer } from '../products/reducers';
import { UsersReducer } from "../users/reducers";
export default function createStore() {
// reduxのcreateStoreメソッドをreturn
return reduxCreateStore(
// combineReducersでstateを生成
combineReducers({
products: ProductsReducer,
users: UsersReducer
})
);
}
combineReducers()とは?
- 分割したReducersをまとめる
- stateのカテゴリ毎
- オブジェクトをreturnする(stateのデータ構造)
combineReducers({
products: ProductsReducer,
users: UsersReducer
})
↓
{
products: {
// productsのstate
},
users: {
isSignedIn: false,
uid: "",
username: ""
}
}
StoreとReactアプリの接続
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import createStore from "./reducks/store/store";
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
export const store = createStore();
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();
react-reduxのProviderとは
- propsにstoreを渡す
→ラップしたコンポーネントにstoreの情報を渡す - Reactコンポーネント内でreact-reduxのconnect関数を使えるようにする
→ReactとReduxを接続してstoreを変更できるように
ルーティングの設定
connected-react-routerを利用してルーティングする
ルーティング用ライブラリ
- react-router v4以降
Reactのルーティング用ライブラリ
Reduxとは関係なく、Reactで利用可能 - connected-react-router
ReduxのStoreでルーティングを管理
react-router v4 & v5と互換性あり
middlewareの導入
import {
createStore as reduxCreateStore,
combineReducers,
applyMiddleware
} from "redux";
import { connectRouter, routerMiddleware } from "connected-react-router";
import { UsersReducer } from "../users/reducers";
export default function createStore(history) {
return reduxCreateStore(
combineReducers({
router: connectRouter(history),
users: UsersReducer
}),
applyMiddleware(
routerMiddleware(history)
)
);
}
StoreとRouterの接続
import { ConnectedRouter } from "connected-react-router";
import * as History from 'history';
const history = History.createBrowserHistory();
export const store = createStore(history);
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();
Routerコンポーネントを作る
import React from "react";
import { Route, Switch } from "react-router";
import { Login, Home } from "./templates";
const Router = () => {
return (
<Switch>
<Route exact path="/login" component={Login} />
<Route exact path="(/)?" component={Home} />
</Switch>
);
};
export default Router;
- Routeコンポーネントでパスとコンポーネントを指定する
- exactは「パスとぴったり一致したら」という意味。
Switchコンポーネントと一緒に使う。
(/)?
スラがあってもなくてもという意味
Routerコンポーネントを使う
import React from "react";
import Router from "./Router";
const App = () => {
return (
<main>
<Router />
</main>
);
};
export default App;
templatesファイルの作成
import React from "react";
import { useDispatch } from "react-redux";
import { push } from "connected-react-router";
const Login = () => {
const dispatch = useDispatch();
return (
<div>
<h2>ログイン</h2>
<button onClick={() => dispatch(push('/'))}>
ログイン
</button>
</div>
);
};
export default Login;
import React from "react";
const Home = () => {
return (
<h2>Home</h2>
);
};
export default Home;
re-ducksパターンでファイルを管理する
ディレクトリ構成のベストプラクティス
re-ducksパターンとは
- Reduxのディレクトリ構成
- ファイルを管理しやすくする
- Ducksパターンから派生
Ducksパターン以前
actions
├ products.js
└ users.js
reducers
├ products.js
└ users.js
問題点:
ファイル名が同じなので、
actionについて書いてるんだっけ?reducerだっけ?となる
Ducksパターン
modules
├ products.js
└ users.js
reducerはactionを参照するんだから一つにまとめちゃえとなったのが
Ducksパターン
re-ducksパターン
users
├ ctions.js
├ index.js
├ operations.js
├ reducers.js
├ selectors.js
└ types.js
なぜre-ducksパターン?
Ducksパターンの課題を解決するため
(ファイルの肥大化)
export const UsersReducer= (state = initialState.users, action) => {
...
};
export const SIGN_IN = "SIGN_IN";
export const signInAction = (userState) => {
...
};
export const SIGN_OUT = "SIGN_OUT";
export const signOutAction = () => {
...
};
re-ducksパターンのメリット
- actionsとreducersがシンプルになる
- ファイルが肥大化しにくくなる
- ファイル毎の役割が明確で管理しやすい
products
├ actions.js
├ index.js
├ operations.js
├ reducers.js
├ selectors.js
└ types.js
users
├ actions.js
├ index.js
├ operations.js
├ reducers.js
├ selectors.js
└ types.js
各ファイルの役割
operations
- 複雑な処理を任せられる
- redux-thunkで非同期処理を制御する
- Actionsを呼び出す
types
- TypeScriptで使う
- 型定義を記述してexport
selectors
- Storeで管理しているstateを参照する関数
- reselectというnpmモジュールを使う
import { createSelector } from "reselect";
const usersSelector = (state) => state.users;
export const getUserId = createSelector(
[usersSelector],
state => state.uid
);
redux-thunk
Redux内の非同期処理を制御する
redux-thunkとは
Reduxで非同期処理を制御するライブラリ
通常のActionsはaction objectを受け取る
= 関数を受け取ることができない
= async/awaitやPromiseを使えない
redux-thunkの導入方法
import thunk from "redux-thunk";
export default function createStore(history) {
return reduxCreateStore(
combineReducers({
router: connectRouter(history),
users: UsersReducer
}),
applyMiddleware(
routerMiddleware(history),
thunk
)
);
}
- モジュールimport
- applayMiddleware()に追加
redux-thunkの基礎文法
export const signIn = (email, password) => {
return async (dispatch, getState) => {
const state = getState();
const isSinedIn = state.users.isSignedIn;
if (!isSinedIn) {
const userData = await emailSignIn(email, password);
dispatch(signInAction({
isSinedIn: true,
uid: '00001',
username: 'hoge'
}))
}
};
};
コンテナの役割
Storeと接続されたコンテナコンポーネントを作る
コンテナコンポーネントはStoreとコンポーネントの中継役
コンテナコンポーネントの役割
Reduxの世界とReactの世界を繋ぐ
- stateをフィルタリングしてコンポーネントに渡す
- Storeからdispatchする関数=Actionsをコンポーネントに渡す
いつ使う?
コンテナーコンポーネントを使用するのに必要な手続きが面倒
→現在はRedux Hooksを使う方法がオススメ
明示的にstateをフィルタリングしたいとき
connect()の使い方
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);
import React, {Component} from 'react';
export default class LoginClass extends Component {
render() {
return (
<div>
<h2>ログイン</h2>
<button onClick={() => this.props.actions.signIn()}>
ログインする
</button>
</div>
)
}
}
Hooksでの代替方法
mapStateToProps → useSelector()
mapDispatchToProps → useDispatch()