メインコンテンツへスキップ
非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

関数の分解とリデューサーの合成を用いたリデューサーロジックのリファクタリング

さまざまなタイプのサブリデューサー関数の実例と、それらがどのように組み合わさるのかを見ることは有用です。ここでは、単一の大きなリデューサー関数を複数の小さな関数の合成へリファクタリングするデモンストレーションを見ていきましょう。

注記: この例は、完璧に簡潔なコードではなく、コンセプトとリファクタリングのプロセスを説明するために意図的に冗長なスタイルで書かれています。

初期リデューサー

初期のリデューサーが次のようになっていると仮定します:

const initialState = {
visibilityFilter: 'SHOW_ALL',
todos: []
}

function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER': {
return Object.assign({}, state, {
visibilityFilter: action.filter
})
}
case 'ADD_TODO': {
return Object.assign({}, state, {
todos: state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
})
}
case 'TOGGLE_TODO': {
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo
}

return Object.assign({}, todo, {
completed: !todo.completed
})
})
})
}
case 'EDIT_TODO': {
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo
}

return Object.assign({}, todo, {
text: action.text
})
})
})
}
default:
return state
}
}

この関数はかなり短いですが、すでに複雑になりつつあります。私たちは2つの異なる関心事(フィルタリング vs todoリストの管理)を扱っており、ネストによって更新ロジックの可読性が低下し、全体で何が起こっているのかが明確ではありません。

ユーティリティ関数の抽出

最初の良いステップとして、フィールドを更新した新しいオブジェクトを返すユーティリティ関数を作成することが考えられます。また、配列内の特定アイテムを更新しようとする繰り返しパターンを関数として抽出することもできます:

function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues)
}

function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item
}

// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item)
return updatedItem
})

return updatedItems
}

function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER': {
return updateObject(state, { visibilityFilter: action.filter })
}
case 'ADD_TODO': {
const newTodos = state.todos.concat({
id: action.id,
text: action.text,
completed: false
})

return updateObject(state, { todos: newTodos })
}
case 'TOGGLE_TODO': {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})

return updateObject(state, { todos: newTodos })
}
case 'EDIT_TODO': {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { text: action.text })
})

return updateObject(state, { todos: newTodos })
}
default:
return state
}
}

これにより重複が減り、可読性が若干向上しました。

ケースリデューサーの抽出

次に、各ケースを独立した関数に分割できます:

// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}

function setVisibilityFilter(state, action) {
return updateObject(state, { visibilityFilter: action.filter })
}

function addTodo(state, action) {
const newTodos = state.todos.concat({
id: action.id,
text: action.text,
completed: false
})

return updateObject(state, { todos: newTodos })
}

function toggleTodo(state, action) {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})

return updateObject(state, { todos: newTodos })
}

function editTodo(state, action) {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { text: action.text })
})

return updateObject(state, { todos: newTodos })
}

function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(state, action)
case 'ADD_TODO':
return addTodo(state, action)
case 'TOGGLE_TODO':
return toggleTodo(state, action)
case 'EDIT_TODO':
return editTodo(state, action)
default:
return state
}
}

これで各ケースで何が起こっているかが_非常に_明確になりました。いくつかのパターンが浮かび上がってくるのも見て取れます。

ドメイン別のデータ処理の分離

アプリケーションレデューサーは依然としてすべての異なるケースを認識しています。フィルターロジックとtodoロジックを分離してみましょう:

// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}

function setVisibilityFilter(visibilityState, action) {
// Technically, we don't even care about the previous state
return action.filter
}

function visibilityReducer(visibilityState = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(visibilityState, action)
default:
return visibilityState
}
}

function addTodo(todosState, action) {
const newTodos = todosState.concat({
id: action.id,
text: action.text,
completed: false
})

return newTodos
}

function toggleTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})

return newTodos
}

function editTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { text: action.text })
})

return newTodos
}

function todosReducer(todosState = [], action) {
switch (action.type) {
case 'ADD_TODO':
return addTodo(todosState, action)
case 'TOGGLE_TODO':
return toggleTodo(todosState, action)
case 'EDIT_TODO':
return editTodo(todosState, action)
default:
return todosState
}
}

function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}

2つの「状態スライス」リデューサーが全体の状態のうち自分自身の部分のみを引数として受け取るようになったため、複雑なネストした状態オブジェクトを返す必要がなくなり、結果としてシンプルになっている点に注目してください。

ボイラープレートの削減

ほぼ完成です。switch文を好まない人が多いため、アクションタイプとケース関数のルックアップテーブルを作成する関数を使用するのが一般的です。ここではボイラープレート削減で説明されているcreateReducer関数を使用します:

// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}

function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}

// Omitted
function setVisibilityFilter(visibilityState, action) {}

const visibilityReducer = createReducer('SHOW_ALL', {
SET_VISIBILITY_FILTER: setVisibilityFilter
})

// Omitted
function addTodo(todosState, action) {}
function toggleTodo(todosState, action) {}
function editTodo(todosState, action) {}

const todosReducer = createReducer([], {
ADD_TODO: addTodo,
TOGGLE_TODO: toggleTodo,
EDIT_TODO: editTodo
})

function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}

スライス単位でのリデューサーの結合

最後のステップとして、Redux組み込みのcombineReducersユーティリティを使用して、トップレベルのアプリケーションレデューサーの「状態スライス」ロジックを処理できます。最終結果は次のようになります:

// Reusable utility functions

function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues)
}

function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item
}

// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item)
return updatedItem
})

return updatedItems
}

function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}

// Handler for a specific case ("case reducer")
function setVisibilityFilter(visibilityState, action) {
// Technically, we don't even care about the previous state
return action.filter
}

// Handler for an entire slice of state ("slice reducer")
const visibilityReducer = createReducer('SHOW_ALL', {
SET_VISIBILITY_FILTER: setVisibilityFilter
})

// Case reducer
function addTodo(todosState, action) {
const newTodos = todosState.concat({
id: action.id,
text: action.text,
completed: false
})

return newTodos
}

// Case reducer
function toggleTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})

return newTodos
}

// Case reducer
function editTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { text: action.text })
})

return newTodos
}

// Slice reducer
const todosReducer = createReducer([], {
ADD_TODO: addTodo,
TOGGLE_TODO: toggleTodo,
EDIT_TODO: editTodo
})

// "Root reducer"
const appReducer = combineReducers({
visibilityFilter: visibilityReducer,
todos: todosReducer
})

ここで、いくつかのタイプに分割されたリデューサー関数の例が得られました:updateObjectcreateReducerのようなヘルパーユーティリティ、setVisibilityFilteraddTodoのような特定ケースのハンドラー、visibilityReducertodosReducerのような状態スライスハンドラーなどです。また、appReducerが「ルートリデューサー」の例であることもわかります。

この例の最終結果は元のバージョンよりも明らかに長くなっていますが、これは主にユーティリティ関数の抽出、コメントの追加、明確化のための意図的な冗長性(個別のreturn文など)によるものです。各関数を個別に見ると、責任の範囲が小さくなり、意図がより明確になっているはずです。また、実際のアプリケーションでは、これらの関数はreducerUtilities.jsvisibilityReducer.jstodosReducer.jsrootReducer.jsといった別々のファイルに分割されるでしょう。