ボイラープレートの削減
Reduxは、一部はFluxから着想を得ており、Fluxに対する最も一般的な不満は、ボイラープレートを大量に記述する必要があることです。 このレシピでは、個人のスタイル、チームの好み、長期的な保守性などに応じて、Reduxでコードの冗長さをどのように選択できるかを検討します。
アクション
アクションは、アプリで発生した内容を記述するプレーンオブジェクトであり、データを変更する意図を記述する唯一の方法として機能します。 **アクションがディスパッチする必要があるオブジェクトであることはボイラープレートではなく、Reduxの基本的な設計上の選択の1つである**ことが重要です。
Fluxに似ていると主張するフレームワークはありますが、アクションオブジェクトの概念はありません。 予測可能であるという点では、これはFluxやReduxからの後退です。 シリアライズ可能なプレーンオブジェクトアクションがない場合、ユーザーセッションを記録して再生したり、タイムトラベルによるホットリロードを実装したりすることは不可能です。 データを直接変更したい場合は、Reduxは必要ありません。
アクションは次のようになります
{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }
アクションには、reducer(またはFluxのストア)がそれらを識別するのに役立つ定数型があるのが一般的な慣例です。 文字列はシリアライズ可能であり、シンボルを使用すると記録と再生が不必要に難しくなるため、アクションタイプには文字列を使用し、シンボルは使用しないことをお勧めします。
Fluxでは、伝統的にすべてのアクションタイプを文字列定数として定義すると考えられています
const ADD_TODO = 'ADD_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
const LOAD_ARTICLE = 'LOAD_ARTICLE'
これはなぜ有益なのでしょうか? **定数は不要であるとよく言われますが、小規模プロジェクトではこれは正しいかもしれません。** 大規模プロジェクトの場合、アクションタイプを定数として定義することにはいくつかの利点があります
- すべてのアクションタイプが1か所にまとめられているため、名前付けの一貫性を保つのに役立ちます。
- 新しい機能に取り組む前に、既存のすべてのアクションを確認したい場合があります。 必要なアクションがチームの誰かに既に追加されているが、知らなかったという可能性があります。
- プルリクエストで追加、削除、変更されたアクションタイプのリストは、チームの全員が新機能のスコープと実装を追跡するのに役立ちます。
- アクション定数をインポートするときにタイプミスをすると、 `undefined` が返されます。 Reduxはそのようなアクションをディスパッチするとすぐにスローするため、すぐに間違いを見つけることができます。
プロジェクトの規則を選択するのはあなた次第です。 インライン文字列を使用して開始し、後で定数に移行し、さらに後でそれらを1つのファイルにグループ化することもできます。 Reduxはここでは何の意見も持っていないので、あなたの最良の判断を使用してください.
アクションクリエーター
アクションをディスパッチする場所でインラインでアクションオブジェクトを作成する代わりに、それらを生成する関数を作成するのが、もう1つの一般的な慣例です.
たとえば、オブジェクトリテラルで `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'))
アクションクリエーターは、しばしばボイラープレートとして批判されてきました。 まあ、あなたはそれらを書く必要はありません! **これがプロジェクトにより適していると感じる場合は、オブジェクトリテラルを使用できます。** ただし、アクションクリエーターを作成することには、知っておくべきいくつかの利点があります。
デザイナーがプロトタイプのレビュー後に戻ってきて、最大3つのTODOを許可する必要があるとします。 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 }
}
}
Reducer
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を使用すると、同じ更新ロジックをreducer関数として記述できます
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の実際のボイラープレートは概念的なものです。更新を発行する必要があること、ストアをディスパッチャーに登録する必要があること、ストアがオブジェクトである必要があること(そして、ユニバーサルアプリが必要な場合に発生する複雑さ)です。
ドキュメントで `switch` ステートメントを使用しているかどうかに基づいてFluxフレームワークを選択する人がまだ多いのは残念です。 `switch` が気に入らない場合は、以下に示すように、単一の関数でこれを解決できます。
Reducerの生成
アクションタイプからハンドラーへのマッピングとしてreducerをオブジェクトとして表現できる関数を記述してみましょう。 たとえば、 `todos` reducerを次のように定義したい場合
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は、このようなヘルパー関数をデフォルトでは提供していません。それは、記述方法が複数あるためです。プレーンなJSオブジェクトをImmutableオブジェクトに自動的に変換してサーバーの状態をハイドレートしたい場合もあるでしょう。返された状態を現在の状態とマージしたい場合もあるでしょう。「キャッチオール」ハンドラには、さまざまなアプローチがあるかもしれません。これらはすべて、特定のプロジェクトでチームが選択する規約によって異なります。
Redux reducerのAPIは(state, action) => newState
ですが、これらのreducerをどのように作成するかはあなた次第です。