| title | useReducer |
|---|
useReducer は、リデューサ (reducer) をコンポーネントに追加するための React フックです。
const [state, dispatch] = useReducer(reducer, initialArg, init?)リデューサ で state を管理するために、コンポーネントのトップレベルで useReducer を呼び出します。
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...reducer: state をどのように更新するかを指定するリデューサ関数です。純粋でなければならず、引数として state とアクションを取り、次の state を返します。state とアクションはどのような型でも大丈夫です。initialArg: 初期 state が計算される元になる値です。任意の型の値を指定できます。どのように初期 state を計算するかは、次のinit引数に依存します。- 省略可能
init: 初期 state を返す初期化関数です。指定されていない場合、初期 state はinitialArgそのものになります。指定されている場合、初期 state はinit(initialArg)の結果になります。
useReducer は、2 つの値を持つ配列を返します:
- 現在の state。最初のレンダー中に、
init(initialArg)またはinitialArg(initがない場合)が設定されます。 - state を別の値に更新し、再レンダーをトリガするための
dispatch関数。
useReducerはフックなので、コンポーネントのトップレベルまたは独自のカスタムフック内でのみ呼び出すことができます。ループや条件の中で呼び出すことはできません。必要な場合は、新しいコンポーネントとして抜き出し、その中に state を移動させてください。dispatch関数は常に同一のものとなるため、多くの場合エフェクトの依存配列では省略されますが、依存配列に含めてもエフェクトの再実行は起こりません。依存値を削除してもリンタがエラーを出さない場合、削除しても安全です。エフェクトから依存値を取り除く方法を参照してください。- Strict Mode では、React は純粋でない関数を見つけやすくするために、リデューサと初期化関数を 2 回呼び出します。これは開発時の動作であり、本番には影響しません。リデューサと初期化関数が純粋であれば(そうあるべきです)、これはロジックに影響しません。片方の呼び出しの結果は無視されます。
useReducer によって返される dispatch 関数を使うことで、state を別の値に更新して再レンダーをトリガすることができます。dispatch 関数には、唯一の引数としてアクションを渡す必要があります。
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...React がセットする次の state は、引数として state の現在値および dispatch に渡されたアクションを用いて reducer 関数を呼び出した結果になります。
action: ユーザによって実行されたアクションです。任意の型の値を指定できます。慣例として、アクションは通常オブジェクトであり、typeプロパティで識別され、他のプロパティでオプションの追加情報を保持します。
dispatch 関数には返り値はありません。
-
dispatch関数は、次のレンダーのための state 変数のみを更新します。dispatch関数を呼び出した後に state 変数を読み取ると、既に画面上に表示されている、dispatch呼び出し前の古い値を読み取ります。 -
与えられた新しい値が、
Object.isの比較により、現在のstateと同じと判断された場合、React はコンポーネントとその子要素の再レンダーをスキップします。これは最適化です。React は結果を無視できる場合でも必要があってコンポーネントを呼び出すかもしれませんが、これがコードに影響してはいけません。 -
React は state の更新のバッチ処理(batching, 束ね処理) を行います。これにより、すべてのイベントハンドラが実行され、それらの
set関数が呼び出された後に画面が更新されます。これにより、1 つのイベント中に複数回の再レンダーが発生するのを防ぐことができます。稀なケースとして、DOM にアクセスするなどの理由で React に画面を早期に強制的に更新させる必要がある場合は、flushSyncを使用できます。
コンポーネントのトップレベルで useReducer を呼び出して、リデューサを使って state を管理します。
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...useReducer は、2 つの値を持つ配列を返します:
- この state 変数の現在の state。与えられた初期 state が初期値として設定されます。
- ユーザ操作に応じて state を変更するための
dispatch関数。
画面上の内容を更新するには、アクションと呼ばれる、ユーザが行ったことを表すオブジェクトを引数として dispatch を呼び出します。
function handleClick() {
dispatch({ type: 'incremented_age' });
}React は現在の state とアクションをリデューサ関数に渡します。リデューサは次の state を計算して返します。React はその次の state を保存するとともに、その state を使ってコンポーネントをレンダーし、UI を更新します。
import { useReducer } from 'react';
function reducer(state, action) {
if (action.type === 'incremented_age') {
return {
age: state.age + 1
};
}
throw Error('Unknown action.');
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
return (
<>
<button onClick={() => {
dispatch({ type: 'incremented_age' })
}}>
Increment age
</button>
<p>Hello! You are {state.age}.</p>
</>
);
}button { display: block; margin-top: 10px; }useReducer は useState と非常に似ていますが、イベントハンドラから state 更新ロジックをコンポーネントの外の単一の関数に移動することができます。詳しくは、useState と useReducer の選び方を参照ください。
リデューサ関数は次のように宣言されます:
function reducer(state, action) {
// ...
}その後、次の state を計算して返すコードを中に書いていきます。慣例として、switch 文として書くことが一般的です。switch の各 case ごとに、次の state を計算して返します。
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}アクションの形はどのようなものでも構いません。慣例として、アクションを識別するための type プロパティを持ったオブジェクトを渡すことが一般的です。アクションにはリデューサが次の state を計算するために必要な最小限の情報を含めるべきです。
function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...アクションのタイプ名はコンポーネント内にローカルなものです。各アクションは、複数のデータの変更につながるものであっても、単一のユーザ操作を表すようにしてください。state の形は任意ですが、通常はオブジェクトまたは配列になります。
詳しくは、リデューサへの state ロジックの抽出を参照ください。
state は読み取り専用です。state 内のオブジェクトや配列を変更しないでください。
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Don't mutate an object in state like this:
state.age = state.age + 1;
return state;
}代わりに、常に新しいオブジェクトをリデューサから返してください。
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Instead, return a new object
return {
...state,
age: state.age + 1
};
}詳しくは、state 内のオブジェクトの更新と state 内の配列の更新を参照ください。
この例では、リデューサが 2 つのフィールド(name と age)を持つ state オブジェクトを管理しています。
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}
const initialState = { name: 'Taylor', age: 42 };
export default function Form() {
const [state, dispatch] = useReducer(reducer, initialState);
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
return (
<>
<input
value={state.name}
onChange={handleInputChange}
/>
<button onClick={handleButtonClick}>
Increment age
</button>
<p>Hello, {state.name}. You are {state.age}.</p>
</>
);
}button { display: block; margin-top: 10px; }この例では、リデューサがタスクの配列を管理しています。配列は書き換えずに更新する必要があります。
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
export default function TaskApp() {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false }
];import { useState } from 'react';
export default function AddTask({ onAddTask }) {
const [text, setText] = useState('');
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
onAddTask(text);
}}>Add</button>
</>
)
}import { useState } from 'react';
export default function TaskList({
tasks,
onChangeTask,
onDeleteTask
}) {
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
</li>
))}
</ul>
);
}
function Task({ task, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
onChange({
...task,
text: e.target.value
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
taskContent = (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={e => {
onChange({
...task,
done: e.target.checked
});
}}
/>
{taskContent}
<button onClick={() => onDelete(task.id)}>
Delete
</button>
</label>
);
}button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }配列やオブジェクトを書き換えずに更新するのが面倒な場合、Immer のようなライブラリを使用して冗長なコードを減らすことができます。Immer を使用すると、オブジェクトを書き換えているかのような簡潔なコードを書くことができますが、内部ではイミュータブルな更新が行われます。
import { useImmerReducer } from 'use-immer';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
function tasksReducer(draft, action) {
switch (action.type) {
case 'added': {
draft.push({
id: action.id,
text: action.text,
done: false
});
break;
}
case 'changed': {
const index = draft.findIndex(t =>
t.id === action.task.id
);
draft[index] = action.task;
break;
}
case 'deleted': {
return draft.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
export default function TaskApp() {
const [tasks, dispatch] = useImmerReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false },
];import { useState } from 'react';
export default function AddTask({ onAddTask }) {
const [text, setText] = useState('');
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
onAddTask(text);
}}>Add</button>
</>
)
}import { useState } from 'react';
export default function TaskList({
tasks,
onChangeTask,
onDeleteTask
}) {
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
</li>
))}
</ul>
);
}
function Task({ task, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
onChange({
...task,
text: e.target.value
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
taskContent = (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={e => {
onChange({
...task,
done: e.target.checked
});
}}
/>
{taskContent}
<button onClick={() => onDelete(task.id)}>
Delete
</button>
</label>
);
}button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }{
"dependencies": {
"immer": "1.7.3",
"react": "latest",
"react-dom": "latest",
"react-scripts": "latest",
"use-immer": "0.5.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}React は初期 state を一度保存したのち、次回以降のレンダーでは無視します。
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...createInitialState(username) の結果は初期レンダーでのみ使用されますが、レンダーの度に毎回この関数を呼び出しています。これは、大きな配列を作成したり、高コストな計算を行ったりしている場合に無駄になる可能性があります。
これを解決するために、useReducer の 3 番目の引数として初期化関数を渡すことができます:
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...createInitialState を渡していることに注意してください。これは関数そのものであり、createInitialState() ではありません。これにより、初期 state は初期化後に再作成されなくなります。
上記の例では、createInitialState は username 引数を受け取ります。初期化関数が初期 state を計算するために情報を必要としない場合は、useReducer に対して 2 番目の引数として null を渡すことができます。
この例では、初期化関数を渡しているため、createInitialState 関数は初期化時にのみ実行されます。初期化関数は入力フィールドに文字を入力するなどでコンポーネントが再レンダーされたとしても実行されません。
import TodoList from './TodoList.js';
export default function App() {
return <TodoList username="Taylor" />;
}import { useReducer } from 'react';
function createInitialState(username) {
const initialTodos = [];
for (let i = 0; i < 50; i++) {
initialTodos.push({
id: i,
text: username + "'s task #" + (i + 1)
});
}
return {
draft: '',
todos: initialTodos,
};
}
function reducer(state, action) {
switch (action.type) {
case 'changed_draft': {
return {
draft: action.nextDraft,
todos: state.todos,
};
};
case 'added_todo': {
return {
draft: '',
todos: [{
id: state.todos.length,
text: state.draft
}, ...state.todos]
}
}
}
throw Error('Unknown action: ' + action.type);
}
export default function TodoList({ username }) {
const [state, dispatch] = useReducer(
reducer,
username,
createInitialState
);
return (
<>
<input
value={state.draft}
onChange={e => {
dispatch({
type: 'changed_draft',
nextDraft: e.target.value
})
}}
/>
<button onClick={() => {
dispatch({ type: 'added_todo' });
}}>Add</button>
<ul>
{state.todos.map(item => (
<li key={item.id}>
{item.text}
</li>
))}
</ul>
</>
);
}この例では、初期化関数を渡していないため、createInitialState 関数は入力フィールドに文字を入力するなど、レンダーの度に毎回実行されます。動作には目に見える違いはありませんが、効率が下がります。
import TodoList from './TodoList.js';
export default function App() {
return <TodoList username="Taylor" />;
}import { useReducer } from 'react';
function createInitialState(username) {
const initialTodos = [];
for (let i = 0; i < 50; i++) {
initialTodos.push({
id: i,
text: username + "'s task #" + (i + 1)
});
}
return {
draft: '',
todos: initialTodos,
};
}
function reducer(state, action) {
switch (action.type) {
case 'changed_draft': {
return {
draft: action.nextDraft,
todos: state.todos,
};
};
case 'added_todo': {
return {
draft: '',
todos: [{
id: state.todos.length,
text: state.draft
}, ...state.todos]
}
}
}
throw Error('Unknown action: ' + action.type);
}
export default function TodoList({ username }) {
const [state, dispatch] = useReducer(
reducer,
createInitialState(username)
);
return (
<>
<input
value={state.draft}
onChange={e => {
dispatch({
type: 'changed_draft',
nextDraft: e.target.value
})
}}
/>
<button onClick={() => {
dispatch({ type: 'added_todo' });
}}>Add</button>
<ul>
{state.todos.map(item => (
<li key={item.id}>
{item.text}
</li>
))}
</ul>
</>
);
}アクションをディスパッチしたがログには古い state の値が表示される {/ive-dispatched-an-action-but-logging-gives-me-the-old-state-value/}
dispatch 関数を呼び出しても、実行中のコード内の state は変更されません:
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // Request a re-render with 43
console.log(state.age); // Still 42!
setTimeout(() => {
console.log(state.age); // Also 42!
}, 5000);
}これは state がスナップショットのように振る舞うためです。state を更新すると、新しい state の値で再レンダーが要求されますが、既に実行中のイベントハンドラ内の state JavaScript 変数には影響を与えません。
次の state の値を推測する必要がある場合は、リデューサを自分で呼び出すことで手動で計算することができます:
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }React は、Object.is の比較により、次の state が前の state と等しいと判断される場合、更新を無視します。これは通常、state 内のオブジェクトや配列を直接書き換えた場合に発生します:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Wrong: mutating existing object
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Wrong: mutating existing object
state.name = action.nextName;
return state;
}
// ...
}
}既存の state オブジェクトを書き換えて返しているため、React は更新を無視します。これを修正するには、常に state 内のオブジェクトの更新や state 内の配列の更新を正しく行い、これらの書き換えを避ける必要があります。
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Correct: creating a new object
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Correct: creating a new object
return {
...state,
name: action.nextName
};
}
// ...
}
}ディスパッチ後にリデューサからの state の一部が undefined になる {/a-part-of-my-reducer-state-becomes-undefined-after-dispatching/}
新しい state を返す際に、すべての case の分岐においてすべての既存のフィールドをコピーするようになっているか確認してください:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Don't forget this!
age: state.age + 1
};
}
// ...上記の ...state を省略すると、返される次の state には age フィールドのみが含まれ、他のフィールドは何も含まれなくなってしまいます。
ディスパッチ後にリデューサからの state 全体が undefined になる {/my-entire-reducer-state-becomes-undefined-after-dispatching/}
state が予期せず undefined になる場合、おそらくいずれかの case で state を return し忘れているか、アクションのタイプがいずれの case とも一致していないからです。原因を見つけるために、switch の外でエラーをスローしてみましょう。
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}また、このようなミスをキャッチするために、TypeScript のような静的型チェッカを使用することもできます。
Too many re-renders. React limits the number of renders to prevent an infinite loop. というエラーが表示されることがあります。通常、これはレンダー中にアクションを無条件でディスパッチしているため、コンポーネントがループに入っていることを意味します:レンダー、ディスパッチ(これにより再度レンダーが発生)、レンダー、ディスパッチ(これにより再度レンダーが発生)、といった繰り返しです。非常に多くの場合、これはイベントハンドラの指定方法のミスによって引き起こされます。
// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>
// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>
// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>このエラーの原因がわからない場合は、コンソールのエラーの横にある矢印をクリックし、JavaScript スタックを調べてエラーの原因となる具体的な dispatch 関数呼び出しを見つけてください。
Strict Mode では、React はリデューサと初期化関数を 2 回呼び出します。これによってコードが壊れることがあってはなりません。
この開発専用の振る舞いは、コンポーネントを純粋に保つために役立ちます。React は 2 回の呼び出しのうちの 1 つの結果を使用し、もう 1 つの結果は無視します。コンポーネント、イニシャライザ、およびリデューサ関数が純粋である限り、これはロジックに影響を与えません。ただし、これらの関数が誤っていて不純である場合、これによりミスに気付くことができます。
例えば、以下の純粋でないリデューサ関数はステート内の配列を書き換えています:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Mistake: mutating state
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}React がリデューサ関数を 2 回呼び出すため、todo が 2 回追加されたことがわかり、ミスがあることがわかります。この例では、配列の書き換えではなく置き換えを行うことでミスを修正できます:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Correct: replacing with new state
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}これでリデューサ関数は純粋になったため、1 回余分に呼び出されても動作に影響はありません。これが React が 2 回呼び出すことでミスを発見できる理由です。コンポーネント、初期化関数、およびリデューサ関数のみが純粋である必要があります。イベントハンドラは純粋である必要はないため、React はイベントハンドラを 2 回呼び出すことはありません。
詳細は、コンポーネントを純粋に保つを参照してください。