Redux Essentials, Part 2: Redux Toolkit アプリ構造
- 一般的なReact + Redux Toolkit アプリの構造
- Redux DevTools Extensionで状態の変化を確認する方法
はじめに
パート1:Reduxの概要と概念では、Reduxの有用性、Reduxコードのさまざまな部分を記述するために使用される用語と概念、およびReduxアプリを介したデータのフローについて説明しました。
それでは、これらの要素がどのように連携するかを実際に動作する例で見ていきましょう。
カウンターアプリの例
ここで見ていくサンプルプロジェクトは、ボタンをクリックすることで数値を増減できる小さなカウンターアプリケーションです。それほど面白くはありませんが、React + Redux アプリケーションの重要な要素をすべて実際に動作している様子を示しています。
このプロジェクトは、Create-React-App の公式Reduxテンプレートを使用して作成されました。すぐに、標準的なReduxアプリケーション構造で構成されており、Redux Toolkitを使用してReduxストアとロジックを作成し、React-Reduxを使用してReduxストアとReactコンポーネントを接続しています。
こちらがプロジェクトのライブバージョンです。右側のアプリプレビューでボタンをクリックして操作し、左側のソースファイルを参照できます。
自分のコンピューターでこのプロジェクトを作成したい場合は、Reduxテンプレートを使用して新しいCreate-React-Appプロジェクトを開始できます。
npx create-react-app redux-essentials-example --template redux
カウンターアプリの使用
カウンターアプリは、使用中に内部で何が起こっているかを観察できるように設定されています。
ブラウザのDevToolsを開きます。次に、DevToolsで「Redux」タブを選択し、右上のツールバーで「State」ボタンをクリックします。次のようになるはずです。
右側には、Reduxストアが次のようなアプリの状態値で開始されていることがわかります。
{
counter: {
value: 0
}
}
DevToolsを使用する際に、ストアの状態がどのように変化するかを示します。
まず、アプリを操作して何が起こるかを確認してみましょう。「+」ボタンをクリックしてから、Redux DevToolsの「Diff」タブを確認します。
ここで2つの重要なことがわかります。
- 「+」ボタンをクリックすると、型が「counter/increment」のアクションがストアにディスパッチされました。
- そのアクションがディスパッチされると、`state.counter.value`フィールドが`0`から`1`に変更されました。
次の手順を試してください。
- 「+」ボタンをもう一度クリックします。表示される値は2になります。
- 「-」ボタンを1回クリックします。表示される値は1になります。
- 「Add Amount」ボタンをクリックします。表示される値は3になります。
- テキストボックスの数字「2」を「3」に変更します。
- 「Add Async」ボタンをクリックします。ボタンにプログレスバーが表示され、数秒後に表示される値が6に変わります。
Redux DevToolsに戻ります。ボタンをクリックした回数だけ、合計5つのアクションがディスパッチされているのがわかります。左側のリストから最後の`「counter/incrementByAmount」`エントリを選択し、右側の「Action」タブをクリックします。
このアクションオブジェクトは次のようになっていることがわかります。
{
type: 'counter/incrementByAmount',
payload: 3
}
そして、「Diff」タブをクリックすると、`state.counter.value`フィールドがそのアクションに応じて`3`から`6`に変更されたことがわかります。
アプリ内部で何が起こっており、状態が時間とともにどのように変化しているかを確認できることは非常に強力です!
DevToolsには、アプリのデバッグに役立つさらに多くのコマンドとオプションがあります。右上の「Trace」タブをクリックしてみてください。パネルにJavaScript関数のスタックトレースが表示され、アクションがストアに到達したときに実行されていた行を示すソースコードのいくつかのセクションが表示されます。特に1つの行が強調表示されているはずです。`<Counter>`コンポーネントからこのアクションをディスパッチしたコード行です。
これにより、特定のアクションをディスパッチしたコードの部分を簡単に追跡できます。
アプリケーションの内容
アプリの動作がわかったところで、その仕組みを見てみましょう。
このアプリケーションを構成する主要なファイルを次に示します。
/src
index.js
:アプリの開始点App.js
:最上位のReactコンポーネント/app
store.js
:Reduxストアインスタンスを作成します。
/features
/counter
Counter.js
:カウンター機能のUIを表示するReactコンポーネントcounterSlice.js
:カウンター機能のReduxロジック
まず、Reduxストアの作成方法を見てみましょう。
Reduxストアの作成
app/store.js
を開きます。次のようになります。
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export default configureStore({
reducer: {
counter: counterReducer
}
})
Reduxストアは、Redux Toolkitの`configureStore`関数を使用して作成されます。`configureStore`には、`reducer`引数を渡す必要があります。
アプリケーションは多くの異なる機能で構成される可能性があり、それらの各機能には独自のreducer関数がある可能性があります。`configureStore`を呼び出す際には、オブジェクト内のすべての異なるreducerを渡すことができます。オブジェクト内のキー名は、最終的な状態値のキーを定義します。
カウンターロジックのreducer関数をエクスポートする`features/counter/counterSlice.js`というファイルがあります。ここでその`counterReducer`関数をインポートし、ストアを作成する際に含めることができます。
`{counter: counterReducer}`のようなオブジェクトを渡すと、Redux状態オブジェクトの`state.counter`セクションを持つことになり、アクションがディスパッチされたときに`counterReducer`関数が`state.counter`セクションの更新の可否と方法を決定することになります。
Reduxでは、さまざまな種類のプラグイン(「ミドルウェア」と「エンハンサー」)を使用してストアの設定をカスタマイズできます。`configureStore`は、デフォルトでいくつかのミドルウェアをストアの設定に追加して、優れた開発者エクスペリエンスを提供し、Redux DevTools Extensionがその内容を検査できるようにストアを設定します。
Redux スライス
**「スライス」とは、アプリ内の単一の機能に関するRedux reducerロジックとアクションのコレクションであり、通常は単一のファイルにまとめて定義されます。**その名前は、ルートRedux状態オブジェクトを複数の状態の「スライス」に分割することから来ています。
たとえば、ブログアプリでは、ストアの設定は次のようになります。
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'
export default configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
}
})
この例では、`state.users`、`state.posts`、`state.comments`はそれぞれ、Redux状態の別々の「スライス」です。`usersReducer`は`state.users`スライスの更新を担当するため、「スライスreducer」関数と呼ばれます。
詳細な説明:reducerと状態構造
Reduxストアは、作成時に単一の「ルートreducer」関数を渡す必要があります。したがって、多くの異なるスライスreducer関数がある場合、単一のルートreducerをどのように取得し、それがReduxストアの状態の内容をどのように定義するのでしょうか?
すべてのスライスreducerを手動で呼び出そうとすると、次のようになる可能性があります。
function rootReducer(state = {}, action) {
return {
users: usersReducer(state.users, action),
posts: postsReducer(state.posts, action),
comments: commentsReducer(state.comments, action)
}
}
これは、各スライスreducerを個別に呼び出し、Redux状態の特定のスライスを渡し、最終的な新しいRedux状態オブジェクトに各戻り値を含めます。
Reduxには、これを自動的に行う`combineReducers`という関数があります。これは、引数としてスライスreducerでいっぱいのオブジェクトを受け取り、アクションがディスパッチされるたびに各スライスreducerを呼び出す関数を返します。各スライスreducerの結果はすべて、最終結果として単一のオブジェクトにまとめられます。`combineReducers`を使用して、前の例と同じことを行うことができます。
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
})
スライスreducerのオブジェクトを`configureStore`に渡すと、ルートreducerを生成するためにそれらを`combineReducers`に渡します。
前述のように、`reducer`引数にreducer関数を直接渡すこともできます。
const store = configureStore({
reducer: rootReducer
})
スライスreducerとアクションの作成
`counterReducer`関数が`features/counter/counterSlice.js`から来ていることがわかっているので、そのファイルの中身を詳しく見ていきましょう。
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
以前、UIのさまざまなボタンをクリックすると、3つの異なるReduxアクションタイプがディスパッチされました。
{type: "counter/increment"}
{type: "counter/decrement"}
{type: "counter/incrementByAmount"}
アクションは`type`フィールドを持つプレーンオブジェクトであり、`type`フィールドは常に文字列であり、通常はアクションオブジェクトを作成して返す「アクションクリエーター」関数があります。では、これらのアクションオブジェクト、タイプ文字列、アクションクリエーターはどこで定義されているのでしょうか?
毎回手動で書くこともできます。しかし、それは面倒です。それに、Reduxで本当に重要なのはreducer関数とその新しい状態を計算するためのロジックです。
Redux Toolkitには、createSlice
という関数があります。これは、アクションタイプの文字列、アクションクリエイター関数、アクションオブジェクトを生成する作業を処理します。あなたがする必要があるのは、このスライスに名前を定義し、いくつかのreducer関数を格納したオブジェクトを作成するだけで、対応するアクションコードが自動的に生成されます。name
オプションの文字列は各アクションタイプの最初の部分として使用され、各reducer関数のキー名は2番目の部分として使用されます。そのため、"counter"
という名前と"increment"
reducer関数は、{type: "counter/increment"}
というアクションタイプを生成します。(結局、コンピューターがやってくれるなら、手作業で書く必要はありません!)
name
フィールドに加えて、createSlice
はreducerの初期状態値を渡す必要があります。これにより、初めて呼び出されたときにstate
が存在します。この場合、value
フィールドが0から始まるオブジェクトを提供しています。
ここに3つのreducer関数があり、それは異なるボタンをクリックすることでディスパッチされた3つの異なるアクションタイプに対応しています。
createSlice
は、私たちが書いたreducer関数と同じ名前のアクションクリエイターを自動的に生成します。それらのいずれかを呼び出して何が返されるかを確認できます。
console.log(counterSlice.actions.increment())
// {type: "counter/increment"}
また、これらのすべてのアクションタイプに対応する方法を知るスライスreducer関数も生成します。
const newState = counterSlice.reducer(
{ value: 10 },
counterSlice.actions.increment()
)
console.log(newState)
// {value: 11}
Reducerのルール
先に述べたように、reducerは常にいくつかの特別なルールに従う必要があります。
state
とaction
の引数に基づいて新しい状態値のみを計算する必要があります。- 既存の
state
を変更することは許可されていません。代わりに、既存のstate
をコピーし、コピーされた値に変更を加えることで、不変の更新を行う必要があります。 - 非同期ロジックやその他の「副作用」を実行することはできません。
しかし、なぜこれらのルールが重要なのでしょうか?いくつかの理由があります。
- Reduxの目標の1つは、コードを予測可能にすることです。関数の出力が入力引数のみから計算される場合、そのコードの動作を理解し、テストすることが容易になります。
- 一方、関数がそれ自身外部の変数に依存したり、ランダムに動作したりする場合、実行したときに何が起こるかを知ることはできません。
- 関数がその引数を含む他の値を変更する場合、アプリケーションの動作が予期せず変わる可能性があります。「状態を更新しましたが、UIが更新されません!」など、バグの一般的な原因となる可能性があります。
- Redux DevToolsの機能の一部は、reducerがこれらのルールに正しく従っていることに依存しています。
「不変の更新」に関するルールは特に重要であり、さらに詳しく説明する価値があります。
Reducerと不変の更新
先に、「mutation」(既存のオブジェクト/配列値の変更)と「immutability」(値を変更できないものとして扱う)について説明しました。
Reduxでは、reducerは決して元の/現在の状態値を変更することは許可されていません!
// ❌ Illegal - by default, this will mutate the state!
state.value = 123
Reduxで状態を変更してはならない理由はいくつかあります。
- UIが最新の値を正しく表示するために更新されないなど、バグを引き起こします。
- 状態がなぜどのように更新されたかを理解するのが難しくなります。
- テストの記述が難しくなります。
- 「タイムトラベルデバッグ」を正しく使用できなくなります。
- Reduxの意図された精神と使用方法に反します。
では、オリジナルを変更できない場合、更新された状態をどのように返すのでしょうか?
Reducerは元の値のコピーのみを作成でき、その後コピーを変更できます。
// ✅ This is safe, because we made a copy
return {
...state,
value: 123
}
JavaScriptの配列/オブジェクトのスプレッド演算子や元の値のコピーを返すその他の関数を使用して、手動で不変の更新を作成できることを既に見てきました。「このように手動で不変の更新を作成するのは、覚えにくく、正しく行うのが難しい」と考えているなら、その通りです!:)
手動で不変の更新ロジックを作成するのは難しいことであり、reducerで状態を誤って変更することは、Reduxユーザーが犯す最も一般的な間違いです。
そのため、Redux ToolkitのcreateSlice
関数は、より簡単な方法で不変の更新を作成できます!
createSlice
は、Immerと呼ばれるライブラリを内部で使用します。Immerは、Proxy
と呼ばれる特別なJSツールを使用して提供されたデータをラップし、「変更」するコードを記述できます。しかし、Immerはあなたが加えようとしたすべての変更を追跡し、その変更のリストを使用して、手動で不変の更新ロジックをすべて記述した場合と同様に、安全に不変に更新された値を返します。
そのため、これの代わりに
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
このように見えるコードを書くことができます。
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}
はるかに読みやすくなりました!
しかし、ここで非常に重要なことを覚えておいてください。
Redux ToolkitのcreateSlice
とcreateReducer
では、内部でImmerを使用しているため、「変更」ロジックのみを記述できます!Immerなしでreducerに変更ロジックを記述すると、状態が変更され、バグが発生します!
それを念頭に置いて、カウンタースライスの実際のリデューサに戻りましょう。
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
increment
reducerは常にstate.value
に1を加算します。Immerはドラフトstate
オブジェクトに変更を加えたことを知っているため、ここでは実際に何も返す必要はありません。同様に、decrement
reducerは1を引きます。
これら2つのreducerでは、コードでaction
オブジェクトを見る必要はありません。それはとにかく渡されますが、必要ないため、reducerのパラメーターとしてaction
を宣言するのを省略できます。
一方、incrementByAmount
reducerは何かを知る必要があります。カウンター値にどれくらい加算する必要があるかです。そのため、reducerはstate
とaction
の両方の引数を持つように宣言します。この場合、テキストボックスに入力した金額がaction.payload
フィールドに入れられていることがわかっているので、それをstate.value
に加算できます。
不変性と不変の更新の記述の詳細については、「不変の更新パターン」のドキュメントページとReactとReduxにおける不変性の完全ガイドを参照してください。
Thunkを使用した非同期ロジックの記述
これまでのところ、アプリケーションのすべてのロジックは同期していました。アクションがディスパッチされ、ストアはreducerを実行して新しい状態を計算し、ディスパッチ関数は終了します。しかし、JavaScript言語には非同期コードを記述する多くの方法があり、私たちのアプリは通常、APIからのデータのフェッチなど、非同期ロジックを持っています。Reduxアプリでその非同期ロジックを配置する場所が必要です。
thunkとは、非同期ロジックを含むことができる特定の種類のRedux関数です。Thunkは2つの関数を使用して記述されます。
- 引数として
dispatch
とgetState
を受け取る内部thunk関数 - thunk関数を生成して返す外部のクリエイター関数
counterSlice
からエクスポートされる次の関数は、thunkアクションクリエイターの例です。
// The function below is called a thunk and allows us to perform async logic.
// It can be dispatched like a regular action: `dispatch(incrementAsync(10))`.
// This will call the thunk with the `dispatch` function as the first argument.
// Async code can then be executed and other actions can be dispatched
export const incrementAsync = amount => dispatch => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
それらは、典型的なReduxアクションクリエイターと同じ方法で使用できます。
store.dispatch(incrementAsync(5))
ただし、thunkを使用するには、redux-thunk
ミドルウェア(Reduxのプラグインの一種)を、作成時にReduxストアに追加する必要があります。幸いなことに、Redux ToolkitのconfigureStore
関数はそれを自動的に設定しているので、ここでthunkを使用できます。
サーバーからデータを取得するためにAJAX呼び出しを行う必要がある場合、その呼び出しをthunkに配置できます。これは少し長く書かれた例なので、それがどのように定義されているかを見ることができます。
// the outside "thunk creator" function
const fetchUserById = userId => {
// the inside "thunk function"
return async (dispatch, getState) => {
try {
// make an async call in the thunk
const user = await userAPI.fetchById(userId)
// dispatch an action when we get the response back
dispatch(userLoaded(user))
} catch (err) {
// If something went wrong, handle it here
}
}
}
thunkはパート5:非同期ロジックとデータフェッチで使用されているのを見ることができます。
詳細な説明:Thunkと非同期ロジック
reducerには非同期ロジックを配置できないことを知っています。しかし、そのロジックはどこかに存在する必要があります。
Reduxストアにアクセスできる場合、非同期コードを記述し、完了時にstore.dispatch()
を呼び出すことができます。
const store = configureStore({ reducer: counterReducer })
setTimeout(() => {
store.dispatch(increment())
}, 250)
しかし、実際のReduxアプリでは、ストアを他のファイル、特にReactコンポーネントにインポートすることは許可されていません。これは、そのコードのテストと再利用を難しくするためです。
さらに、最終的にはあるストアで使用されることがわかっている非同期ロジックを記述する必要があることがよくありますが、どのストアであるかはわかりません。
Reduxストアは「ミドルウェア」で拡張できます。これは、追加の機能を追加できるアドオンまたはプラグインの一種です。ミドルウェアを使用する最も一般的な理由は、非同期ロジックを持つことができるコードを記述しながら、同時にストアと通信できるようにすることです。それらはストアを変更して、dispatch()
を呼び出し、プレーンなアクションオブジェクトではない値(関数やPromiseなど)を渡すこともできます。
Redux Thunkミドルウェアは、関数をdispatch
に渡せるようにストアを変更します。実際、それは十分に短いため、ここに貼り付けることができます。
const thunkMiddleware =
({ dispatch, getState }) =>
next =>
action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
dispatch
に渡された「アクション」が、プレーンなアクションオブジェクトではなく実際に関数かどうかを確認します。それが実際に関数である場合、関数を呼び出し、結果を返します。そうでない場合、これはアクションオブジェクトである必要があるため、アクションをストアに渡します。
これにより、dispatch
とgetState
にアクセスしながら、必要な同期または非同期コードを記述する方法が提供されます。
このファイルにはもう一つ関数がありますが、<Counter>
UIコンポーネントについて説明する際に触れます。
詳しくは、Redux Thunk のドキュメント、記事What the heck is a thunk?、およびRedux FAQ の「非同期処理にミドルウェアを使用する理由」のエントリを参照してください。
React カウンターコンポーネント
先に、スタンドアロンの React <Counter>
コンポーネントがどのようなものかを確認しました。React + Redux アプリケーションには、同様の <Counter>
コンポーネントがありますが、いくつかの点が異なります。
まず、Counter.js
コンポーネントファイルを見ていきましょう。
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount
} from './counterSlice'
import styles from './Counter.module.css'
export function Counter() {
const count = useSelector(selectCount)
const dispatch = useDispatch()
const [incrementAmount, setIncrementAmount] = useState('2')
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
</div>
{/* omit additional rendering output here */}
</div>
)
}
以前のプレーンな React の例と同様に、useState
フックにいくつかのデータを格納するCounter
という関数コンポーネントがあります。
しかし、このコンポーネントでは、実際の現在のカウンター値を状態として格納しているようには見えません。count
という変数がありますが、それはuseState
フックから来ていません。
React にはuseState
やuseEffect
などのいくつかの組み込みフックがありますが、他のライブラリはReactのフックを使用してカスタムロジックを構築する独自のカスタムフックを作成できます。
React-Reduxライブラリには、ReactコンポーネントがReduxストアとやり取りできるようにするカスタムフックのセットがあります。
useSelector
を使用したデータの読み取り
まず、useSelector
フックにより、コンポーネントはReduxストアの状態から必要なデータ部分を抽出できます。
以前、state
を引数として受け取り、状態値の一部を返す「セレクター」関数を作成できることを確認しました。
counterSlice.js
には、このセレクター関数が最後にあります。
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state) => state.counter.value)`
export const selectCount = state => state.counter.value
Reduxストアにアクセスできれば、現在のカウンター値を次のように取得できます。
const count = selectCount(store.getState())
console.log(count)
// 0
コンポーネントファイルにストアをインポートすることは許可されていないため、コンポーネントはReduxストアに直接アクセスできません。しかし、useSelector
はバックグラウンドでReduxストアとのやり取りを処理します。セレクター関数を渡すと、someSelector(store.getState())
を呼び出し、結果を返します。
したがって、現在のストアのカウンター値を取得するには、次のようにします。
const count = useSelector(selectCount)
既にエクスポートされているセレクターのみを使用する必要はありません。たとえば、useSelector
へのインライン引数としてセレクター関数を記述できます。
const countPlusTwo = useSelector(state => state.counter.value + 2)
アクションがディスパッチされ、Reduxストアが更新されるたびに、useSelector
はセレクター関数を再実行します。セレクターが前回とは異なる値を返す場合、useSelector
はコンポーネントが新しい値で再レンダリングされるようにします。
useDispatch
を使用したアクションのディスパッチ
同様に、Reduxストアにアクセスできれば、store.dispatch(increment())
のように、アクションクリエイターを使用してアクションをディスパッチできることがわかっています。ストア自体にアクセスできないため、dispatch
メソッドにアクセスする方法が必要です。
useDispatch
フックはそれを行い、Reduxストアからの実際のdispatch
メソッドを提供します。
const dispatch = useDispatch()
そこから、ユーザーがボタンをクリックするなど、何か操作をしたときにアクションをディスパッチできます。
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
コンポーネントの状態とフォーム
ここで、「アプリケーションのすべての状態をReduxストアに入れる必要がありますか?」と疑問に思うかもしれません。
答えはいいえです。アプリケーション全体で必要となるグローバル状態はReduxストアに格納する必要があります。1か所でのみ必要な状態は、コンポーネントの状態に保持する必要があります。
この例では、ユーザーがカウンターに追加する次の数値を入力できる入力テキストボックスがあります。
const [incrementAmount, setIncrementAmount] = useState('2')
// later
return (
<div className={styles.row}>
<input
className={styles.textbox}
aria-label="Set increment amount"
value={incrementAmount}
onChange={e => setIncrementAmount(e.target.value)}
/>
<button
className={styles.button}
onClick={() => dispatch(incrementByAmount(Number(incrementAmount) || 0))}
>
Add Amount
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}
>
Add Async
</button>
</div>
)
入力のonChange
ハンドラーでアクションをディスパッチし、それをreducerに保持することで、現在の数値文字列をReduxストアに保持することもできます。しかし、それは何のメリットもありません。そのテキスト文字列が使用されるのは、この<Counter>
コンポーネントの中だけです。(確かに、この例にはもう1つのコンポーネント<App>
しかありません。しかし、多くのコンポーネントを持つ大規模なアプリケーションであっても、この入力値を気にするのは<Counter>
だけです。)
そのため、その値を<Counter>
コンポーネントのuseState
フックに保持するのが理にかなっています。
同様に、isDropdownOpen
というブール型のフラグがある場合、アプリケーションの他のコンポーネントはそれを気にしません。これは本当にこのコンポーネントのローカルに留まるべきです。
React + Redux アプリケーションでは、グローバル状態はReduxストアに、ローカル状態はReactコンポーネントに保持する必要があります。
どこに配置するかわからない場合は、Reduxにどのようなデータを入れるべきかを判断するための一般的な経験則を次に示します。
- アプリケーションの他の部分がこのデータに関心を持っていますか?
- この元のデータに基づいて、さらに派生データを作成する必要がありますか?
- 複数のコンポーネントを駆動するために同じデータが使用されていますか?
- 特定の時点の状態を復元できること(つまり、タイムトラベルデバッグ)に価値がありますか?
- データをキャッシュしますか(つまり、既に存在する状態を使用するのではなく、再要求しますか)?
- UIコンポーネントのホットリロード中にこのデータを整合性を保ちますか(交換時に内部状態が失われる可能性があります)?
これは、一般的にReduxにおけるフォームの考え方の良い例でもあります。**ほとんどのフォームの状態はReduxに保持するべきではありません。**代わりに、編集中にフォームコンポーネントにデータを保持し、ユーザーが完了したときにReduxアクションをディスパッチしてストアを更新します。
先に進む前に、もう1つ注意すべき点があります。counterSlice.js
のincrementAsync
thunkを覚えていますか?このコンポーネントでそれを利用しています。他の通常のaction creatorsをディスパッチするのと同じ方法で使用していることに注意してください。このコンポーネントは、通常のactionをディスパッチしているのか、非同期ロジックを開始しているのかを気にしません。ボタンをクリックすると何かをディスパッチするというだけです。
ストアの提供
コンポーネントはuseSelector
とuseDispatch
フックを使用してReduxストアとやり取りできることがわかりました。しかし、ストアをインポートしなかったため、これらのフックはどのReduxストアとやり取りするのかをどのように知っていますか?
このアプリケーションのさまざまな部分をすべて確認したので、このアプリケーションの出発点に戻り、パズルの最後のピースがどのように組み合わさるのかを確認しましょう。
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'
import * as serviceWorker from './serviceWorker'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
常にReactDOM.render(<App />)
を呼び出して、Reactにルート<App>
コンポーネントのレンダリングを開始するように指示する必要があります。useSelector
のようなフックが正しく動作するためには、<Provider>
というコンポーネントを使用して、バックグラウンドでReduxストアを渡し、アクセスできるようにする必要があります。
app/store.js
でストアを作成したので、ここでインポートできます。次に、<Provider>
コンポーネントを<App>
全体に配置し、ストアを渡します:<Provider store={store}>
。
これで、useSelector
またはuseDispatch
を呼び出すReactコンポーネントはすべて、<Provider>
に渡したReduxストアとやり取りします。
学習内容
カウンターの例アプリは非常に小さいですが、React + Reduxアプリの主要な部分が連携して動作する様子を示しています。以下は、私たちが扱った内容です。
- Redux Toolkitの
configureStore
APIを使用してReduxストアを作成できます。configureStore
は、名前付き引数としてreducer
関数を قبولします。configureStore
は、最適なデフォルト設定でストアを自動的に設定します。
- Reduxロジックは通常、「スライス」と呼ばれるファイルに整理されます。
- 「スライス」には、Redux状態の特定の機能/セクションに関連するreducerロジックとアクションが含まれています。
- Redux Toolkitの
createSlice
APIは、提供する各reducer関数について、アクションクリエイターとアクションタイプを生成します。
- Redux reducerは特定のルールに従う必要があります。
state
とaction
引数に基づいて新しい状態値のみを計算する必要があります。- 既存の状態をコピーすることによって、不変の更新を行う必要があります。
- 非同期ロジックやその他の「副作用」を含めることはできません。
- Redux Toolkitの
createSlice
APIはImmerを使用して、「変更可能な」不変の更新を可能にします。
- 非同期ロジックは通常、「thunk」と呼ばれる特別な関数で記述されます。
- Thunkは、
dispatch
とgetState
を引数として受け取ります。 - Redux Toolkitは、デフォルトで
redux-thunk
ミドルウェアを有効にします。
- Thunkは、
- React-Reduxにより、ReactコンポーネントはReduxストアとやり取りできます。
- アプリケーションを
<Provider store={store}>
でラップすると、すべてのコンポーネントがストアを使用できるようになります。 - グローバル状態はReduxストアに、ローカル状態はReactコンポーネントに保持する必要があります。
- アプリケーションを
次のステップ
Reduxアプリケーションのすべてのピースが実際に動作している様子を確認したので、独自のアプリケーションを作成する時です!このチュートリアルの残りの部分では、Reduxを使用するより大きな例アプリを作成します。その過程で、Reduxを正しく使用するために知っておく必要があるすべての重要な概念を説明します。
例アプリの作成を開始するには、パート3:基本的なReduxデータフローに進んでください。