このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
ボイラープレートの削減
Reduxは一部Fluxに影響を受けており、Fluxに関する最も一般的な不満は、大量のボイラープレートコードを書かせることです。このレシピでは、個人のスタイルやチームの好み、長期的な保守性などに応じて、Reduxがコードの冗長さをどのように選択可能にするかを考察します。
アクション
アクションはアプリ内で発生した事象を記述するプレーンオブジェクトであり、データ変更の意図を表現する唯一の手段です。ディスパッチするアクションがオブジェクト形式であることはボイラープレートではなく、Reduxの基本設計原則の一つであるという点が重要です。
Fluxに類似すると主張するフレームワークの中には、アクションオブジェクトの概念を持たないものがあります。これは予測可能性の観点ではFluxやReduxより退歩です。シリアライズ可能なプレーンオブジェクトのアクションがなければ、ユーザーセッションの記録・再生やタイムトラベル付きホットリロードの実装が不可能です。データを直接変更したい場合は、Reduxは不要です。
アクションの例:
{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }
アクションが定数タイプを持つのは一般的な慣習で、リデューサー(またはFluxのStore)が識別するのに役立ちます。アクションタイプにはSymbolではなく文字列を使用することを推奨します。文字列はシリアライズ可能であり、Symbolを使用すると記録・再生が不必要に複雑化するためです。
Fluxでは伝統的に、すべてのアクションタイプを文字列定数として定義します:
const ADD_TODO = 'ADD_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
const LOAD_ARTICLE = 'LOAD_ARTICLE'
なぜ有益なのでしょうか?定数は不要と主張されることもありますし、小規模プロジェクトでは確かにその通りかもしれません。 しかし大規模プロジェクトでは、アクションタイプを定数として定義することに次の利点があります:
-
すべてのアクションタイプが単一箇所に集約されるため、命名の一貫性が保たれる
-
新機能開発前に既存アクションを確認できる。必要なアクションがチームメンバーによって既に追加されている可能性がある
-
プルリクエストで追加・削除・変更されたアクションタイプの一覧により、新機能の範囲と実装をチーム全体で把握できる
-
アクション定数のインポート時にタイポがあると
undefinedになる。Reduxはそのようなアクションをディスパッチ時に即時エラー送出するため、早期に間違いを発見できる
プロジェクトの慣習は自由に選択可能です。インライン文字列から始め、後に定数へ移行し、さらに単一ファイルへ集約することもできます。Reduxはここに特定の意見を持たないため、最善の判断をしてください。
アクションクリエーター
もう一つの一般的な慣習として、アクションをディスパッチする場所でインラインオブジェクトを作成する代わりに、生成関数を作成する方法があります。
例えば、オブジェクトリテラルでdispatchを呼び出す代わりに:
// somewhere in an event handler
dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
})
別ファイルでアクションクリエーターを定義し、コンポーネントにインポートできます:
actionCreators.js
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
AddTodo.js
import { addTodo } from './actionCreators'
// somewhere in an event handler
dispatch(addTodo('Use Redux'))
アクションクリエーターはボイラープレートと批判されることがありますが、書かなくても構いません!プロジェクトに適しているならオブジェクトリテラルを使用してください。 ただし、アクションクリエーターを作成する利点もいくつか存在します。
デザイナーがプロトタイプをレビュー後、Todoは最大3つまでと制限する必要があると連絡してきたとします。この制限を実装するには、redux-thunkミドルウェアを用いてアクションクリエーターをコールバック形式で書き換え、早期リターンを追加します:
function addTodoWithoutCheck(text) {
return {
type: 'ADD_TODO',
text
}
}
export function addTodo(text) {
// This form is allowed by Redux Thunk middleware
// described below in “Async Action Creators” section.
return function (dispatch, getState) {
if (getState().todos.length === 3) {
// Exit early
return
}
dispatch(addTodoWithoutCheck(text))
}
}
addTodoアクションクリエーターの動作を変更しましたが、呼び出し元のコードには完全に透過的です。TODOが追加されるすべての場所をチェックする必要がなくなります。 アクションクリエーターを使用すると、アクション発行に関する追加ロジックを、実際にアクションを発行するコンポーネントから分離できます。これはアプリケーションが活発に開発中で要件が頻繁に変更される場合に非常に便利です。
アクションクリエーターの生成
Flummoxのような一部のフレームワークは、アクションクリエーター関数定義から自動的にアクションタイプ定数を生成します。このアイデアは、ADD_TODO定数とaddTodo()アクションクリエーターの両方を定義する必要がないというものです。内部的には、このようなソリューションもアクションタイプ定数を生成しますが、暗黙的に作成されるため間接的な層が追加され、混乱を招く可能性があります。明示的にアクションタイプ定数を作成することをお勧めします。
単純なアクションクリエーターを書くのは面倒で、冗長なボイラープレートコードが生成されることがよくあります:
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
export function editTodo(id, text) {
return {
type: 'EDIT_TODO',
id,
text
}
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}
アクションクリエーターを生成する関数を常に書くことができます:
function makeActionCreator(type, ...argNames) {
return function (...args) {
const action = { type }
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index]
})
return action
}
}
const ADD_TODO = 'ADD_TODO'
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
export const addTodo = makeActionCreator(ADD_TODO, 'text')
export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'text')
export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')
redux-actやredux-actionsなどのユーティリティライブラリも、アクションクリエーターの生成を支援します。これらはボイラープレートコードを削減し、Flux Standard Action(FSA)などの標準への準拠を強制するのに役立ちます。
非同期アクションクリエーター
ミドルウェアを使用すると、ディスパッチされる前のすべてのアクションオブジェクトを解釈するカスタムロジックを注入できます。非同期アクションはミドルウェアの最も一般的なユースケースです。
ミドルウェアなしでは、dispatchはプレーンオブジェクトのみを受け入れるため、コンポーネント内でAJAX呼び出しを実行する必要があります:
actionCreators.js
export function loadPostsSuccess(userId, response) {
return {
type: 'LOAD_POSTS_SUCCESS',
userId,
response
}
}
export function loadPostsFailure(userId, error) {
return {
type: 'LOAD_POSTS_FAILURE',
userId,
error
}
}
export function loadPostsRequest(userId) {
return {
type: 'LOAD_POSTS_REQUEST',
userId
}
}
UserInfo.js
import { Component } from 'react'
import { connect } from 'react-redux'
import {
loadPostsRequest,
loadPostsSuccess,
loadPostsFailure
} from './actionCreators'
class Posts extends Component {
loadData(userId) {
// Injected into props by React Redux `connect()` call:
const { dispatch, posts } = this.props
if (posts[userId]) {
// There is cached data! Don't do anything.
return
}
// Reducer can react to this action by setting
// `isFetching` and thus letting us show a spinner.
dispatch(loadPostsRequest(userId))
// Reducer can react to these actions by filling the `users`.
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch(loadPostsSuccess(userId, response)),
error => dispatch(loadPostsFailure(userId, error))
)
}
componentDidMount() {
this.loadData(this.props.userId)
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.loadData(this.props.userId)
}
}
render() {
if (this.props.isFetching) {
return <p>Loading...</p>
}
const posts = this.props.posts.map(post => (
<Post post={post} key={post.id} />
))
return <div>{posts}</div>
}
}
export default connect(state => ({
posts: state.posts,
isFetching: state.isFetching
}))(Posts)
しかし、異なるコンポーネントが同じAPIエンドポイントからデータを要求するため、これはすぐに繰り返しになります。さらに、このロジックの一部(例:キャッシュされたデータが利用可能な場合の早期終了)を多くのコンポーネントで再利用したいと考えています。
ミドルウェアを使用すると、より表現力豊かな非同期アクションクリエーターを記述できます。 プレーンオブジェクト以外のものをディスパッチし、その値を解釈できます。例えば、ミドルウェアはディスパッチされたPromiseを「キャッチ」し、それらをリクエストと成功/失敗のアクションのペアに変換できます。
ミドルウェアの最も単純な例はredux-thunkです。「Thunk」ミドルウェアは、アクションクリエーターを「thunk」、つまり関数を返す関数として記述できるようにします。 これは制御を反転させます:dispatchを引数として取得できるため、複数回ディスパッチするアクションクリエーターを記述できます。
メモ
Thunkミドルウェアはミドルウェアの一例に過ぎません。ミドルウェアは「関数をディスパッチできるようにすること」が目的ではありません。使用する特定のミドルウェアが処理方法を知っている任意のものをディスパッチできるようにすることが目的です。Thunkミドルウェアは関数をディスパッチする際に特定の動作を追加しますが、実際の動作は使用するミドルウェアに依存します。
redux-thunkで書き直したコードを考えてみましょう:
actionCreators.js
export function loadPosts(userId) {
// Interpreted by the thunk middleware:
return function (dispatch, getState) {
const { posts } = getState()
if (posts[userId]) {
// There is cached data! Don't do anything.
return
}
dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
})
// Dispatch vanilla actions asynchronously
fetch(`http://myapi.com/users/${userId}/posts`).then(
response =>
dispatch({
type: 'LOAD_POSTS_SUCCESS',
userId,
response
}),
error =>
dispatch({
type: 'LOAD_POSTS_FAILURE',
userId,
error
})
)
}
}
UserInfo.js
import { Component } from 'react'
import { connect } from 'react-redux'
import { loadPosts } from './actionCreators'
class Posts extends Component {
componentDidMount() {
this.props.dispatch(loadPosts(this.props.userId))
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.props.dispatch(loadPosts(this.props.userId))
}
}
render() {
if (this.props.isFetching) {
return <p>Loading...</p>
}
const posts = this.props.posts.map(post => (
<Post post={post} key={post.id} />
))
return <div>{posts}</div>
}
}
export default connect(state => ({
posts: state.posts,
isFetching: state.isFetching
}))(Posts)
これはタイピング量が大幅に少なくなります!必要であれば、コンテナのloadPostsアクションクリエーターから使用するloadPostsSuccessのような「通常の」アクションクリエーターを保持することもできます。
最後に、独自のミドルウェアを書くことができます。 上記のパターンを一般化し、非同期アクションクリエーターを次のように記述したいとしましょう:
export function loadPosts(userId) {
return {
// Types of actions to emit before and after
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
// Check the cache (optional):
shouldCallAPI: state => !state.posts[userId],
// Perform the fetching:
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
// Arguments to inject in begin/end actions
payload: { userId }
}
}
このようなアクションを解釈するミドルウェアは次のようになります:
function callAPIMiddleware({ dispatch, getState }) {
return next => action => {
const { types, callAPI, shouldCallAPI = () => true, payload = {} } = action
if (!types) {
// Normal action: pass it on
return next(action)
}
if (
!Array.isArray(types) ||
types.length !== 3 ||
!types.every(type => typeof type === 'string')
) {
throw new Error('Expected an array of three string types.')
}
if (typeof callAPI !== 'function') {
throw new Error('Expected callAPI to be a function.')
}
if (!shouldCallAPI(getState())) {
return
}
const [requestType, successType, failureType] = types
dispatch(
Object.assign({}, payload, {
type: requestType
})
)
return callAPI().then(
response =>
dispatch(
Object.assign({}, payload, {
response,
type: successType
})
),
error =>
dispatch(
Object.assign({}, payload, {
error,
type: failureType
})
)
)
}
}
一度applyMiddleware(...middlewares)に渡した後、すべてのAPI呼び出しアクションクリエーターを同じ方法で記述できます:
export function loadPosts(userId) {
return {
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
shouldCallAPI: state => !state.posts[userId],
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
payload: { userId }
}
}
export function loadComments(postId) {
return {
types: [
'LOAD_COMMENTS_REQUEST',
'LOAD_COMMENTS_SUCCESS',
'LOAD_COMMENTS_FAILURE'
],
shouldCallAPI: state => !state.comments[postId],
callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`),
payload: { postId }
}
}
export function addComment(postId, message) {
return {
types: [
'ADD_COMMENT_REQUEST',
'ADD_COMMENT_SUCCESS',
'ADD_COMMENT_FAILURE'
],
callAPI: () =>
fetch(`http://myapi.com/posts/${postId}/comments`, {
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ message })
}),
payload: { postId, message }
}
}
リデューサー
Reduxは更新ロジックを関数として記述することで、Fluxストアの定型コードを大幅に削減します。関数はオブジェクトよりもシンプルで、クラスよりもはるかにシンプルです。
次のFluxストアを考えてみましょう:
const _todos = []
const TodoStore = Object.assign({}, EventEmitter.prototype, {
getAll() {
return _todos
}
})
AppDispatcher.register(function (action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
const text = action.text.trim()
_todos.push(text)
TodoStore.emitChange()
}
})
export default TodoStore
Reduxでは、同じ更新ロジックをリデューサー関数として記述できます:
export function todos(state = [], action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
const text = action.text.trim()
return [...state, text]
default:
return state
}
}
switch文自体が真の定型コードというわけではありません。Fluxの真の定型コードは概念的なものです。つまり、更新の発行が必要であること、StoreをDispatcherに登録する必要があること、Storeがオブジェクトである必要があること(そしてユニバーサルアプリを構築したい場合に生じる複雑さ)です。
多くの開発者がドキュメントでswitch文を使用しているかどうかだけでFluxフレームワークを選ぶのは残念なことです。switchが気に入らない場合は、後述するように単一の関数で解決できます。
リデューサーの生成
アクションタイプからハンドラーへのマッピングオブジェクトとしてリデューサーを表現できる関数を作成しましょう。たとえば、todosリデューサーを次のように定義したいとします:
export const todos = createReducer([], {
[ActionTypes.ADD_TODO]: (state, action) => {
const text = action.text.trim()
return [...state, text]
}
})
これを実現するには、次のヘルパー関数を作成できます:
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
難しくなかったでしょう?Reduxはデフォルトでこのようなヘルパー関数を提供していません。なぜなら、実装方法が多岐にわたるからです。プレーンなJavaScriptオブジェクトをImmutableオブジェクトに自動変換してサーバーステートをハイドレートしたい場合もあるでしょう。返されたステートを現在のステートとマージしたい場合もあるかもしれません。「すべてキャッチ」ハンドラーのアプローチもさまざまです。これらはすべて、特定のプロジェクトでチームが選択する規約に依存します。
ReduxのリデューサーAPIは(state, action) => newStateですが、これらのリデューサーをどのように作成するかは自由です。