Redux
Reduxの仕組み
Actions
Action
は個々のイベントに対する情報のことです。Reduxでは状態変化は全てActionを介して行います。Actionは固有の名前を持ち、状態変化について必要な情報を持つ必要があります。
Reducers
Actionを使って状態変化を生じさせるには、Reducer
と呼ばれる関数にActionを送信する必要があります。また、このようにReducerにActionを送信する動作をDispatch
と呼びます。
Store
Store
はアプリケーションの状態を保持するオブジェクトのことです。ReactやVue.jsのようなフレームワークからStoreを参照することによってUIにStoreを反映させることができます。
Fluxアーキテクチャ
Flux パターン
Fluxはデータの流れを一方向に限定することで状態遷移を単純にし、データ状態の複雑さを軽減させようとする考え方である。
データは以下の流れで変更されていく
- 何らかの
Action
があり、Dispatcher
がそのすべてを受ける Dispatcher
がStore
にAction
を伝搬Store
はDispatcher
から受けたAction
によってデータの状態を変更Store
のデータ状態がView
に渡されるView
がStore
から受けたデータを表示View
で何らかのAction
が発生 -> 1に戻る
メリット
すべてのデータのオペレーションがDispatcherに集約されており、ActionによるStoreの変更は予め決められているため、Actionの発行後のアプリケーションの状態が予測可能になる。
Reduxの原則
- Single source of truth (信頼できる唯一の情報源)
- State is read-only (状態は読み取り専用)
- Changes are made with pure functions (変更は純粋関数にて行われる)
Viewから発行されたActionはDispatcherで割り振られてReducerに渡される。ReducerはそのActionと現在のStateを受け取って新しいStateを返す。
基本的な使い方
Install
$ nmp install react-redux
or
$ yarn add react-redux
Provider
React Redux は<Provider />
を提供します。これによりアプリの他の部分でReduxストアが利用可能になります。
React Reduxアプリの任意のReactコンポーネントを接続できるため、ほとんどのアプリケーションは、アプリのコンポーネントツリー全体を内部に持つ
- index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
Actions
ReduxのActionとそれを生成するAction Creater
と呼ばれる関数群を定義する。
- actions/counterAction.ts
export enum CounterActionType {
ADD = "COUNTER/ADD",
DECREMENT = "COUNTER/DECREMENT",
INCREMENT = "COUNTER/INCREMENT"
}
export interface CounterAction {
type: CounterActionType;
payload?: { amount: number };
}
export const add = (amount: number): CounterAction => ({
payload: { amount },
type: CounterActionType.ADD
});
export const increment = (): CounterAction => ({
type: CounterActionType.INCREMENT
});
export const decrement = (): CounterAction => ({
type: CounterActionType.DECREMENT
});
Actionのフォーマット
interface Action {
type: string;
payload?: Object;
error?: boolean;
meta?: Object;
}
- type: Actionの種類を一意に示す文字列、enum、またはストリングリテラル。必須。
- payload: 主に正常時はActionの実行に必要なデータ。異常時にはエラー情報を格納する。省略可能。
- error: trueの場合、エラーのActionであることを意味する。省略可能。
- meta: その他の追加で必要な情報を格納する。省略可能。
https://qiita.com/kabosu3d/items/e00af944529a5bf89d23
Reducer
以前の状態とアクションを組み合わせて、新しい状態を生み出す役割
- 初期状態はReducerのデフォルト引数で定義される
- 状態を変更する際、渡されてきた
state
をのもを書き換えずに、新しいものを合成するように記述する
import { Reducer } from "redux";
import { CounterAction, CounterActionType } from "../actions/counterActions";
export interface CounterState {
count: number;
}
export const initState: CounterState = { count: 0 };
const counterReducer: Reducer<CounterState, CounterAction> = (
state: CounterState = initState,
action: CounterAction
): CounterState => {
switch (action.type) {
case CounterActionType.ADD:
return {
...state,
count: state.count + (action.payload?.amount || 0)
};
case CounterActionType.INCREMENT:
return {
...state,
count: state.count + 1
};
case CounterActionType.DECREMENT:
return {
...state,
count: state.count - 1
};
default: {
return state;
}
}
};
export
ComponentとStoreの接続(HOCによる書き方)
7.1.0以降はHooksインタフェースが提供されているため、新規の場合、この書き方は忘れていい
React Reduxtはコンポーネントをストアに接続するためのconnect
関数を提供しています。以下を行いコンポーネントとStoreのやり取りを担う。
- 参照したい
Store State
の値をコンポーネントのPropsにマッピングする - 発行したいActionを生成する
Action Creator関数
をPropsにマッピングする
import React, { Component } from "react";
import { Dispatch } from "redux";
import { connect } from "react-redux";
import { increment, decrement, add } from "./actions/counterActions";
import { CounterState } from "./reducers/counterReducer";
import "./styles.css";
interface CounterProps {
count?: number;
add?: (amount: number) => void;
increment?: () => void;
decrement?: () => void;
}
class App extends Component<CounterProps, {}> {
render() {
const {
count = 0,
increment = () => {},
decrement = () => {},
add = () => {}
} = this.props;
return (
<div>
Clicked: {count} times.
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={() => add(10)}>10</button>
</div>
);
}
}
// Storeを受け取ってPropsを返す
const mapStateToProps = (state: CounterState): CounterProps => ({
count: state.count
});
// Dispatcheを受け取ってPropsを返す。dispatch()に渡して対応させたいAction Creator関数にリンクさせる
const mapDispatchToProps = (dispatch: Dispatch): CounterProps => ({
add: (amount) => dispatch(add(amount)),
increment: () => dispatch(increment()),
decrement: () => dispatch(decrement())
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
Hooks
7.1.0以降に追加されたAPIで、connect()
でコンポーネントをラップすることなく、Reduxストアにサブスクライブしてアクションをディスパッチできます。
useSelector
storeから任意のstateの値を抽出することができる。
const result: any = useSelector(selector: Function, equalityFn?: Function)
selectorは概念的にはconnect
のmapStateToProps引数とほぼ同等。関数コンポーネントがrenderされるたびに実行される。useSelector()
はReduxストアにサブスクライブし、ActionがDispatchされるたびにselectorを実行する。
1つの関数コンポーネント内で複数回呼び出すことが可能。 useSelector()を呼び出すたびに、Reduxストアへの個別のサブスクリプションが作成されます。 React Reduxv7で使用されるReactupdateバッチ動作のため、同じコンポーネント内の複数のuseSelector()が新しい値を返すようにするディスパッチされたアクションは、1回の再レンダリングのみになります。
import { FC } from "react";
import { useSelector } from "react-redux";
interface CounterState {
count: number
}
export const CounterComponent: FC = () => {
const count = useSelector<CounterState, number>((state) => state.count);
return (
<div>
{count}
</div>
)
}
useDispatch
actionをdispatchする、dispatcherに渡すための関数を取得する
import { React, FC } from "react";
import { useDispatch } from "react-redux";
export const CounterComponent: FC = () => {
const dispatch = useDispatch()
return (
<div>
<button onClick={() => dispatch({ type: "INCREMENT" })}>
Increment
</button>
</div>
)
}
useStore
<Provider>
に渡されたstore
への参照を返す。頻繁に使用するべきではないが、reducerの交換等に役立つ
Redux Toolkit
https://redux-toolkit.js.org/
Reduxの開発をサポートしてくれるTool。
Install
# NPM
$ npm install @reduxjs/toolkit
# Yarn
$ yarn add @reduxjs/toolkit
configuStore
createStore
をラップして、簡略化された構成オプションと適切なデフォルトを提供します。スライスリデューサーを自動的に組み合わせ、提供するReduxミドルウェアを追加し、デフォルトでredux-thunk
を含め、Redux DevToolsExtensionの使用を可能にします。
Parameters
reducer
ルートreducerとして使用される単一のreducer関数または、combineReducer()
に渡されるslice reducerのオブジェクト
middleware
使用したいRedux middlewareを配列でわたすことで、使用可能になる
devTools
Redux Devtools
を使用するかのフラグ
preloadedState
createStore
関数に渡されるinitial state
enhancers
Redux sotre enhancer
の配列を指定する
- Reduxのdispath()関数をラップした処理が記述されたミドルウェア
Example
- シンプルな使い方
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducers";
const store = configureStore({ reducer: rootReducer });
createAction
指定されたアクションタイプ文字列のアクションクリエーター関数を生成します。関数自体にはtoString()が定義されているため、型定数の代わりに使用できます。
function createAction(type, prepareAction?)
Example
- 往来のactionの書き方
const INCREMENT = "counter/increment";
const increment = (amount: number) => {
return {
type: INCREMENT,
payload: amount
}
}
- Redux Toolkitを使用した書き方
import { createAction } from "@reduxjs/toolkit";
const increment = createAction<number | undefined>("counter/increment");
prepare callbackを使用したActionのカスタマイズ
デフォルトでは、生成されたAction Createは単一の引数受け入れがaction.payload
になります。しかしAction Createrの複数のパラメータを受け入れたり、ランダムIDの生成、現在のタイムスタンプの取得など、ペイロード値の作成をカスタマイズするための追加のロジックを記述したい場合があります。これを行うために、createActionは、オプションの2番目の引数である「preparecallback」を受け入れます。これは、ペイロード値を作成するために使用されます。
import { createAction, nanoid } from "@reduxjs/toolkit";
const addTodo = createAction("tods/add", function prepare(text: string) {
return {
payload: {
text,
id: nanoid(),
createdAt: new Date().toISOString(),
}
}
})
createReducer
これにより、switchステートメントを記述するのではなく、アクションタイプのルックアップテーブルをケースリデューサー関数に提供できます。さらに、Immer
ライブラリを自動的に使用して、通常の変更コードを使用して、より単純な不変の更新を記述できるようにします。
Example
- 往来のreducerの書き方
const initialState = { value: 0 };
const counterReducer = (state = initalState, action) {
switch(action.type) {
case "increment":
return { ...state, value: state.value + 1 };
case "decrement":
return { ...state, value: state.value - 1 };
case "add":
return { ...state, value: state.value + action.payload };
default:
return state;
}
}
- Redux Toolkitを使用した書き方
import { createAction, createReducer, PayloadAction } from "@reduxjs/toolkit";
const increment = createAction("counter/increment");
const decrement = createAction("counter/decrement");
const add = createAction<number>("counter/add");
const initialState = { value: 0 } as CounterState;
const counterReducer = createReducer(initialState, {
[increment.type]: (state) => ({
...state,
value: state.value + 1
}),
[decrement.type]: (state) => ({
...state,
value: state.value - 1
}),
[add.type]: (state, action: PayloadAction<number>) => ({
...state,
value: state.value + action.payload
})
}
- Builderを使用した書き方
より厳密な型安全性が必要な場合は、このAPIが推奨されている
import { createAction, createReducer } from "@reduxjs/toolkit";
interface CounterState {
value: number
}
// Action
const increment = createAction("counter/increment");
const decrement = createAction("counter/decrement");
const add = createAction<number>("counter/add");
const initialState = { value: 0 } as CounterState;
const counterReducer = createReducer(initialState, (builder) => {
builder
.addCase(increment, (state, action) => {
state.value++;
})
.addCase(decrement, (state, action) => {
state.value--;
})
.addCase(add, (state, action) => {
state.value += action.payload
})
})
createSlice
レデューサー関数のオブジェクト、スライス名、および初期状態値を受け入れ、対応するアクション作成者とアクションタイプを使用してスライスレデューサーを自動的に生成します。
Example
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface CounterState {
value: number
}
const initialState = { value: 0 } as CounterState;
const counterSlice = createSlice({
name: "counter", // Actionタイプで使用される名前
initialState, // reducerの初期state
reducers: { // case reducerのオブジェクト
increment(state){ // reducerを追加するために使われるbuilder callback関数、または case reducerの追加オブジェクト。(keyは他のオブジェクトにする必要がある)
state.value++
},
decrement(state){
state.value--
},
incrementByAmount(state, action: PayloadAction<number>) {
state.value += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
extraReducers
Reduxの重要な概念の1つは、各スライスレデューサーがその状態のスライスを「所有」し、多くのスライスレデューサーが同じアクションタイプに独立して応答できることです。 extraReducers
を使用すると、createSlice
は、生成したタイプ以外の他のアクションタイプに応答できます。
extraReducers
で指定されたケースリデューサーは「外部」アクションを参照することを目的としているため、slice.actionsで生成されたアクションはありません。
extraReducers
を使用するための推奨される方法は、ActionReducerMapBuilder
インスタンスを受け取るコールバックを使用することです。
import { createAction, createSLice, Action, AnyAction } from "@reduxjs/toolkit";
const incrementBy = createAction<number>("incrementBy");
const decrement = createAction("decrement");
interface RejectedAction extends Action {
error: Error
}
function isRejectedAction(action: AnyAction): action is RejectedAction {
return action.type.endsWith("rejected");
}
createSlice({
name: "counter",
intialState: 0,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(incrementBy, (state, action) => {
// TSを使用していす場合,actionはここで正しく推測されます
})
.addCase(decrement, (state, action) => {})
.addMatcher(
isRejectedAction,
(state, action) =>
)
.addDefaultCase((state, action) => {})
}
})
Return Value
{
name: string,
reducer: ReducerFunction,
actions: Record<string, ActionCreator>,
caseReducers: Record<string, CaseReducer>
}
createAsyncThunk
アクションタイプの文字列とpromiseを返す関数を受け入れ、そのpromiseに基づいて保留中/実行済み/拒否済みのアクションタイプをディスパッチするThunkを生成します
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { userAPI } from "./userAPI";
const fetchUserById = createAsyncThunk(
"users/fetchByIdStatus",
async(userId, thunkAPI) => {
const response = await userAPI.fetchById(userId);
return response.data;
}
)
const usersSlice = createSlice({
name: "users",
initialState: { entities: [], loading: "idle" },
reducers: {
},
extraReducers: {
[fetchUserById.fulfilled]: (state, action) => {
state.entities.push(action.payload)
}
}
})
dispatch(fetchUserById(123))
Parameter
type
非同期リクエストを生成するための、Redux action type定数を生成するための文字列を渡す
payloadCreator
いくつかの非同期ロジックの結果を含むpromiseを返す必要があるコールバック関数。また、同期的に値を返す場合もあります。エラーが発生した場合は、Errorインスタンスを含む拒否されたプロミス、説明的なエラーメッセージなどのプレーンな値、またはthunkAPI.rejectWithValue関数によって返されるRejectWithValue引数を使用した解決済みのPromiseを返す必要があります。
createEntityAdapter
ストア内の正規化されたデータを管理するための再利用可能なレデューサーとセレクターのセットを生成します
import {
createEntityAdapter,
createSlice,
configureStore,
} from '@reduxjs/toolkit'
type Book = { bookId: string; title: string }
const booksAdapter = createEntityAdapter<Book>({
// Assume IDs are stored in a field other than `book.id`
selectId: (book) => book.bookId,
// Keep the "all IDs" array sorted based on book titles
sortComparer: (a, b) => a.title.localeCompare(b.title),
})
const booksSlice = createSlice({
name: 'books',
initialState: booksAdapter.getInitialState(),
reducers: {
// Can pass adapter functions directly as case reducers. Because we're passing this
// as a value, `createSlice` will auto-generate the `bookAdded` action type / creator
bookAdded: booksAdapter.addOne,
booksReceived(state, action) {
// Or, call them as "mutating" helpers in a case reducer
booksAdapter.setAll(state, action.payload.books)
},
},
})
const store = configureStore({
reducer: {
books: booksSlice.reducer,
},
})
type RootState = ReturnType<typeof store.getState>
console.log(store.getState().books)
// { ids: [], entities: {} }
// Can create a set of memoized selectors based on the location of this entity state
const booksSelectors = booksAdapter.getSelectors<RootState>(
(state) => state.books
)
// And then use the selectors to retrieve values
const allBooks = booksSelectors.selectAll(store.getState())
Parameter
selectId
単一のエンティティインスタンスを受け入れ、内部にある一意のIDフィールドの値を返す関数。指定しない場合、デフォルトの実装はentity => entity.idです。エンティティタイプがentity.id以外のフィールドに一意のID値を保持する場合は、selectId関数を指定する必要があります。
sortComparer
2つのエンティティインスタンスを受け入れるコールバック関数。標準のArray.sort()数値結果(1、0、-1)を返し、並べ替えの相対的な順序を示す必要があります。
提供されている場合、state.ids配列はエンティティオブジェクトの比較に基づいて並べ替えられた順序で保持されるため、ID配列をマッピングしてIDでエンティティを取得すると、エンティティの並べ替えられた配列になります。
Redux Style Guid
https://redux.js.org/style-guide/style-guide/
優先度A : 必須
- stateを直接書き換えない
- reducerに副作用を持たせない
- シリアライズできない値をstateやactionに入れない
- storeはひとつのアプリにつきひとつだけ
Actionに関するルール
- actionをsetterではなくイベントとしてモデリングする
- actionの名前は意味を的確に表現したものにする
- action タイプ名を「ドメインモデル/イベント種別」のフォーマットで書く(キャメルケース)
- Redux Toolkitの
createSlice
ではdomain/action
の形式でactionを生成する
- Redux Toolkitの
- actionをFSAに準拠させる
- dispatchするactionは直に書かずaction createrを使って生成する
ツールやデザインパターンの利用に関するルール
- Reduxのロジックを書くときは
Redux Toolkit
を使う - イミュータブルな状態の更新には
Immer
を使う - デバッグには
Redux DevTools
拡張を使う - ファイル構造には「Feature Folder」または
Ducksパターン
を適用する
その他設計に関するルール
- どの状態をどこに持たせるかは柔軟に考える
- フォームの状態をReduxに入れない
- 複雑なロジックはコンポーネントの外に追い出す
- 非同期処理には
Redux Thunk
を使う