概要
少し前まではVue.jsを書いていたのですが、最近Reactを勉強し始めてReduxの壁が立ちはだかりました。。。
Vuexと違って複雑すぎるReduxが中々自分の中で整理できなかったので、改めて整理しました。
この記事はReduxの使い方の説明やReduxの概念を深く追求したりすることが目的ではなく、あくまで自分の中でReduxの仕組みなどを頭の中で整理し、
その整理したものを実装の仕方と結びつけることを目的として書いたただのメモです
あと冒頭の部分は自分の中でしっくりきた解釈なので間違っていたらご指摘頂けると幸いです。
Reduxの目的
Vuexを用いてvue開発をしていた時は vuex = バケツリレー防止剤
的な用途で使っていて、Reduxも同じ感覚で使うと思っていたけどバケツリレー防止剤
ではないらしい(vuexもそうかもしれん)
バケツリレーを防ぐだけならコンテクストを使えば良いしじゃあ何のために使うのか?
Reduxはデータの流れを把握しやすくするためのツール
っていう認識で今は思っています。
Reduxを使わない場合、様々なコンポーネント内でapiからデータをfetchして、propsで子コンポーネントに渡して...
とデータの流れがわかりづらくなる
Reduxを使用すると、fetchしたデータをReduxのstoreで管理して取得したいコンテナコンポーネントでstoreとconnectし、
プレゼンテーショナルコンポーネントにpropsとして渡して描画するというシンプルなデータの流れになるためメリットがある
ただし、ReduxはVuexなどと比べるとコード量も多いし、あまり規模が大きくないアプリ内では使用しない方が良い
(小さいアプリではデータの流れを把握しづらい、という状況はよほどのスパゲティコード出ない限り起きないため?)
Redux と コンポーネント内のstate
よくReduxとコンポーネントのstateどちらで状態を持たせるか議論されるが、一つの見解として
apiから取得するようなドメイン
はReduxで管理し、UIの状態
はstateで管理するというものがある。
以下のサイトを参考にさせて頂きましたが、自分の中でとてもしっくりきた
https://numb86-tech.hatenablog.com/entry/2019/02/23/224342
ドメイン駆動開発(DDD)と画面駆動開発
要はreduxのstoreにドメインの状態を管理させるか画面の状態を管理させるか?
ドメイン駆動開発が良いとされる。storeでドメインを管理することでapiの複雑さなどをstoreで吸収する
コンポーネントはUIに専念させる
UIのためだけに使用するデータ(例えばフォーム内のテキストなど)をReduxで管理してしまうと
「Reduxはデータの流れを把握しやすくするためのツール」という本質から逸脱してしまって状態管理がわけわからんくなりそう
うん、あんまりここらへんはよく理解してないので参考にした以下のサイトをみてください
Vuexを例に説明しているけど基本的な考え方は変わらないと思うので、結構よかたっ
https://medium.com/studist-dev/ddd-vuex-c47055f6c1ba
構成
基本の流れ
Action Creator(actionを生成) -> Action(dispatchで渡す) -> Reducer(stateの変更をする) -> State(データの反映) -> View
↑ |
---------------------------------------------------------------------------------------------------
Action
- 実態: Object
- 役割: stateを変更するreducerにどのような変更を行うか(type)と必要であれば変更を行う材料(payload(プロパティ名はなんでも可))を表すデータ構造
typeは必須であり文字列を渡す(慣習として文字列は大文字のスネークケース,定数と一緒)
type以外のプロパティは任意でありreducerに渡したいデータを指定できる
actionはdispatch関数の引数に渡すことでreducerに渡せる
{ type: "ACTION_TYPE", payload: hogehoge,.... }
Action Creator
- 実態: 関数
- 返り値: Action
- 役割: 上記で説明したactionを生成するための関数
ここにロジックを書くことが多い(apiを叩いてapiから受け取ったデータを補正するなど)
またapiを叩くなどの非同期処理をaction creator内で実行したい場合は返り値はaction(object)ではなく、dispatchを引数として受け取る関数を返すようにする(redux-thunkなどのライブラリが必要)
import axios from "axios";
import { ADD_TODO, DEL_TODO, FETCH_TODOS } from "./types";
// export const ADD_TODO = "ADD_TODO"; みたいに定義してるファイルから読み込み
// 同期処理の場合
export const addTodo = (title, content) => {
return {
type: ADD_TODO,
payload: { title, content }
}
};
export const delTodo = (id) => {
return {
type: DEL_TODO,
payload: id
};
}
// 非同期処理の場合
export const fetchTodos = () => async dispatch => {
const { data } = await axios('/api/v1/todos');
dispatch({
type: FETCH_TODOS,
payload: data
});
};
Reducer
- 実態: 純粋関数
- 第一引数: state(初期値を必ず設定), 第二引数: action
- 返り値: state
- 役割: dispatchによりactionが送られてきたタイミングで発火(初回storeオブジェクト作成時にも呼び出される)し、stateを変更する
慣習として、switch分でtype別にstateの変更処理を書くことが多い(2つにしか分岐しないならif文でも)
コンポーネントのstateと同様に、プリミティブ型以外のstateはイミュータブルに変更する必要がある
import { ADD_TODO, DEL_TODO, FETCH_TODOS } from "../actions/types";
const initialState = [];
// stateは以下のような構造を想定
// [
// {
// id: 1,
// title: "タイトル",
// content: "内容"
// },
// .
// .
// ]
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
// 一個前のtodoのidから+1した値を取得
const id = state.length > 0 ? state.slice(-1)[0].id + 1 : 1;
const todo = { id, ...action.payload };
return [ ...state, todo ];
case DEL_TODO:
const filterTodos = state.filter( todo => todo.id !== action.payload );
return filterTodos;
case FETCH_TODOS:
return [...state, ...action.payload];
default:
return state;
}
};
export default todoReducer;
State
データの本体
Reducerの中にあるイメージ?(よくわからん)
combineReducers関数を使うことでアプリ内で複数のstate, reducerを持つことができる
Store
Reduxの本体?
createStore関数によって生成する
createStore関数
- 第一引数: reducer(複数のreducerを用いたい場合はcombineReducers関数で生成したRootReducer)
- 第二引数: stateの初期値(reducerで指定することがほとんどなので大体省略する)
- 第三引数: middleware(applyMiddlewareで登録したもの)
import { createStore, applyMiddleware, compose } from "redux";
import rootReducer from "./reducers";
import thunk from "redux-thunk";
const initialState = [];
const middleware = applyMiddleware(thunk);
const store = createStore(
rootReducer,
initialState, // initialStateは省略可
middleware
);
export default store;
上記の場合はRootReducerを指定しているが、Reducerを一つのみしか使用しない場合はそのままReducer関数を渡せば良い
RootReducerは以下のように生成
以下は一つのみしか指定してないが、複数指定可
import { combineReducers } from "redux";
import todoReducer from "./todos";
const rootReducer = combineReducers({
todos: todoReducer, // このように指定すると後述する「propsとstateのマッピング(関数)」で`state.todos(state.プロパティ名)`でstateを取得できる
});
export default rootReducer;
middleware
- dispatch時にreducer実行前に実行してほしい処理を挟むことができる
- redux-thunkとかデバッグツールとかを挟むことが多い
- middlewareはapplyMiddleware関数に渡すことで登録できる
React x Redux
Provider
- react-reduxライブラリが提供するコンポーネント
- storeを使用したいコンポーネントをProviderコンポーネントでラップする
- Providerにはpropsとしてstateオブジェクトを渡す
- 大体アプリのルートコンポーネント(App)をラップする?
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from "react-redux";
import store from "./store";
ReactDOM.render(
<Provider store={ store }>
<App />
</Provider>,
document.getElementById('root')
);
Connect
- ReactでReduxを使用する際にはconnectする必要がある
- connect関数を使う
- connectするコンポーネントをコンテナコンポーネントとか言ったりする
- connectされたコンポーネントはpropsでstateとaction creator関数を使用できる
import React from 'react';
import { connect } from "react-redux";
import { delTodo, fetchTodos } from "../actions/todo";
class TodoList extends React.Component {
componentWillMount() {
this.props.fetchTodos();
}
render() {
const todos = this.props.todos.map(todo => {
return (
<div key={ todo.id }>
<span>{ todo.id } / { todo.title } / { todo.content }</span>
<button onClick={ () => this.props.delTodo(todo.id) }>Delete</button>
</div>
);
});
return (
<>
{ todos }
</>
);
}
}
const mapStateToProps = (state) => {
return {
todos: state.todos
}
};
const mapDispatchToProps = (dispatch) => {
return {
delTodo: id => dispatch(delTodo(id)),
fetchTodos: () => dispatch(fetchTodos())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
コードは以下で説明
connect関数
- 第一引数: propsとstateのマッピング
- 第二引数: propsとaction creator関数のマッピング
- connect関数の返り値である関数を呼び出す際の引数: connectするコンポーネントオブジェクト
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
propsとstateのマッピング(関数)
- 第一引数: state
- 返り値: props(key)とstate(value)を対応させたobject
const mapStateToProps = (state) => {
return {
todos: state.todos // connectしたコンポーネント内で`this.props.todos`で受け取れる(関数コンポーネントの場合は`props.todos`)
}
};
propsとaction creator関数のマッピング
- 第一引数: dispatch関数
- 返り値: props(key)とactionをdispatchする関数(value)を対応させたobject
const mapDispatchToProps = (dispatch) => {
return {
delTodo: id => dispatch(delTodo(id)), // connectしたコンポーネント内で`this.props.delTodo(id)`で呼び出せる(関数コンポーネントの場合は`props.delTodo(id)`)
fetchTodos: () => dispatch(fetchTodos())
};
まとめ
ReactとReduxを合わせて使う場合Redux Hooksを用いればconnectする必要がなかったり、
Redux Toolkitを用いればAction CreatorやReducerめっちゃ簡潔に書けたりしますが、基本は大事ってことで
それらを使う場合でもこの基本の流れはおさえて置きたいものですね