diff --git a/beta/src/content/learn/extracting-state-logic-into-a-reducer.md b/beta/src/content/learn/extracting-state-logic-into-a-reducer.md index b389d6f39..b2d2293bc 100644 --- a/beta/src/content/learn/extracting-state-logic-into-a-reducer.md +++ b/beta/src/content/learn/extracting-state-logic-into-a-reducer.md @@ -4,7 +4,7 @@ title: Extracting State Logic into a Reducer -Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a *reducer.* +Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a _reducer._ @@ -17,14 +17,14 @@ Components with many state updates spread across many event handlers can get ove -## Consolidate state logic with a reducer {/*consolidate-state-logic-with-a-reducer*/} +## Consolidate state logic with a reducer {/* consolidate-state-logic-with-a-reducer */} As your components grow in complexity, it can get harder to see at a glance all the different ways in which a component's state gets updated. For example, the `TaskApp` component below holds an array of `tasks` in state and uses three different event handlers to add, remove, and edit tasks: ```js App.js -import { useState } from 'react'; +import {useState} from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; @@ -32,35 +32,36 @@ export default function TaskApp() { const [tasks, setTasks] = useState(initialTasks); function handleAddTask(text) { - setTasks([...tasks, { - id: nextId++, - text: text, - done: false - }]); + setTasks([ + ...tasks, + { + id: nextId++, + text: text, + done: false, + }, + ]); } function handleChangeTask(task) { - setTasks(tasks.map(t => { - if (t.id === task.id) { - return task; - } else { - return t; - } - })); + setTasks( + tasks.map((t) => { + if (t.id === task.id) { + return task; + } else { + return t; + } + }) + ); } function handleDeleteTask(taskId) { - setTasks( - tasks.filter(t => t.id !== taskId) - ); + setTasks(tasks.filter((t) => t.id !== taskId)); } return ( <>

Prague itinerary

- +
@@ -188,41 +187,44 @@ Reducers are a different way to handle state. You can migrate from `useState` to 2. **Write** a reducer function. 3. **Use** the reducer from your component. -### Step 1: Move from setting state to dispatching actions {/*step-1-move-from-setting-state-to-dispatching-actions*/} +### Step 1: Move from setting state to dispatching actions {/* step-1-move-from-setting-state-to-dispatching-actions */} -Your event handlers currently specify *what to do* by setting state: +Your event handlers currently specify _what to do_ by setting state: ```js function handleAddTask(text) { - setTasks([...tasks, { - id: nextId++, - text: text, - done: false - }]); + setTasks([ + ...tasks, + { + id: nextId++, + text: text, + done: false, + }, + ]); } function handleChangeTask(task) { - setTasks(tasks.map(t => { - if (t.id === task.id) { - return task; - } else { - return t; - } - })); + setTasks( + tasks.map((t) => { + if (t.id === task.id) { + return task; + } else { + return t; + } + }) + ); } function handleDeleteTask(taskId) { - setTasks( - tasks.filter(t => t.id !== taskId) - ); + setTasks(tasks.filter((t) => t.id !== taskId)); } ``` Remove all the state setting logic. What you are left with are three event handlers: -* `handleAddTask(text)` is called when the user presses "Add". -* `handleChangeTask(task)` is called when the user toggles a task or presses "Save". -* `handleDeleteTask(taskId)` is called when the user presses "Delete". +- `handleAddTask(text)` is called when the user presses "Add". +- `handleChangeTask(task)` is called when the user toggles a task or presses "Save". +- `handleDeleteTask(taskId)` is called when the user presses "Delete". Managing state with reducers is slightly different from directly setting state. Instead of telling React "what to do" by setting state, you specify "what the user just did" by dispatching "actions" from your event handlers. (The state update logic will live elsewhere!) So instead of "setting `tasks`" via an event handler, you're dispatching an "added/changed/deleted a task" action. This is more descriptive of the user's intent. @@ -238,14 +240,14 @@ function handleAddTask(text) { function handleChangeTask(task) { dispatch({ type: 'changed', - task: task + task: task, }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', - id: taskId + id: taskId, }); } ``` @@ -258,13 +260,13 @@ function handleDeleteTask(taskId) { // "action" object: { type: 'deleted', - id: taskId + id: taskId, } ); } ``` -It is a regular JavaScript object. You decide what to put in it, but generally it should contain the minimal information about *what happened*. (You will add the `dispatch` function itself in a later step.) +It is a regular JavaScript object. You decide what to put in it, but generally it should contain the minimal information about _what happened_. (You will add the `dispatch` function itself in a later step.) @@ -280,7 +282,7 @@ dispatch({ -### Step 2: Write a reducer function {/*step-2-write-a-reducer-function*/} +### Step 2: Write a reducer function {/* step-2-write-a-reducer-function */} A reducer function is where you will put your state logic. It takes two arguments, the current state and the action object, and it returns the next state: @@ -296,20 +298,23 @@ To move your state setting logic from your event handlers to a reducer function 1. Declare the current state (`tasks`) as the first argument. 2. Declare the `action` object as the second argument. -3. Return the *next* state from the reducer (which React will set the state to). +3. Return the _next_ state from the reducer (which React will set the state to). Here is all the state setting logic migrated to a reducer function: ```js function tasksReducer(tasks, action) { if (action.type === 'added') { - return [...tasks, { - id: action.id, - text: action.text, - done: false - }]; + return [ + ...tasks, + { + id: action.id, + text: action.text, + done: false, + }, + ]; } else if (action.type === 'changed') { - return tasks.map(t => { + return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { @@ -317,7 +322,7 @@ function tasksReducer(tasks, action) { } }); } else if (action.type === 'deleted') { - return tasks.filter(t => t.id !== action.id); + return tasks.filter((t) => t.id !== action.id); } else { throw Error('Unknown action: ' + action.type); } @@ -334,14 +339,17 @@ The code above uses if/else statements, but it's a convention to use [switch sta function tasksReducer(tasks, action) { switch (action.type) { case 'added': { - return [...tasks, { - id: action.id, - text: action.text, - done: false - }]; + return [ + ...tasks, + { + id: action.id, + text: action.text, + done: false, + }, + ]; } case 'changed': { - return tasks.map(t => { + return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { @@ -350,7 +358,7 @@ function tasksReducer(tasks, action) { }); } case 'deleted': { - return tasks.filter(t => t.id !== action.id); + return tasks.filter((t) => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); @@ -365,7 +373,6 @@ If you're not yet comfortable with switch statements, using if/else is completel - Although reducers can "reduce" the amount of code inside your component, they are actually named after the [`reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) operation that you can perform on arrays. @@ -390,40 +397,33 @@ import tasksReducer from './tasksReducer.js'; let initialState = []; let actions = [ - { type: 'added', id: 1, text: 'Visit Kafka Museum' }, - { type: 'added', id: 2, text: 'Watch a puppet show' }, - { type: 'deleted', id: 1 }, - { type: 'added', id: 3, text: 'Lennon Wall pic' }, + {type: 'added', id: 1, text: 'Visit Kafka Museum'}, + {type: 'added', id: 2, text: 'Watch a puppet show'}, + {type: 'deleted', id: 1}, + {type: 'added', id: 3, text: 'Lennon Wall pic'}, ]; -let finalState = actions.reduce( - tasksReducer, - initialState -); +let finalState = actions.reduce(tasksReducer, initialState); const output = document.getElementById('output'); -output.textContent = JSON.stringify( - finalState, - null, - 2 -); +output.textContent = JSON.stringify(finalState, null, 2); ``` ```js tasksReducer.js -export default function tasksReducer( - tasks, - action -) { +export default function tasksReducer(tasks, action) { switch (action.type) { case 'added': { - return [...tasks, { - id: action.id, - text: action.text, - done: false - }]; + return [ + ...tasks, + { + id: action.id, + text: action.text, + done: false, + }, + ]; } case 'changed': { - return tasks.map(t => { + return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { @@ -432,7 +432,7 @@ export default function tasksReducer( }); } case 'deleted': { - return tasks.filter(t => t.id !== action.id); + return tasks.filter((t) => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); @@ -451,12 +451,12 @@ You probably won't need to do this yourself, but this is similar to what React d -### Step 3: Use the reducer from your component {/*step-3-use-the-reducer-from-your-component*/} +### Step 3: Use the reducer from your component {/* step-3-use-the-reducer-from-your-component */} Finally, you need to hook up the `tasksReducer` to your component. Make sure to import the `useReducer` Hook from React: ```js -import { useReducer } from 'react'; +import {useReducer} from 'react'; ``` Then you can replace `useState`: @@ -473,7 +473,7 @@ const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); The `useReducer` Hook is similar to `useState`—you must pass it an initial state and it returns a stateful value and a way to set state (in this case, the dispatch function). But it's a little different. -The `useReducer` Hook takes two arguments: +The `useReducer` Hook takes two arguments: 1. A reducer function 2. An initial state @@ -488,15 +488,12 @@ Now it's fully wired up! Here, the reducer is declared at the bottom of the comp ```js App.js -import { useReducer } from 'react'; +import {useReducer} from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { - const [tasks, dispatch] = useReducer( - tasksReducer, - initialTasks - ); + const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); function handleAddTask(text) { dispatch({ @@ -509,23 +506,21 @@ export default function TaskApp() { function handleChangeTask(task) { dispatch({ type: 'changed', - task: task + task: task, }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', - id: taskId + id: taskId, }); } return ( <>

Prague itinerary

- + { + return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { @@ -554,7 +552,7 @@ function tasksReducer(tasks, action) { }); } case 'deleted': { - return tasks.filter(t => t.id !== action.id); + return tasks.filter((t) => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); @@ -564,57 +562,52 @@ function tasksReducer(tasks, action) { 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 } + {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}, ]; ``` ```js AddTask.js hidden -import { useState } from 'react'; +import {useState} from 'react'; -export default function AddTask({ onAddTask }) { +export default function AddTask({onAddTask}) { const [text, setText] = useState(''); return ( <> setText(e.target.value)} + onChange={(e) => setText(e.target.value)} /> - + - ) + ); } ``` ```js TaskList.js hidden -import { useState } from 'react'; +import {useState} from 'react'; -export default function TaskList({ - tasks, - onChangeTask, - onDeleteTask -}) { +export default function TaskList({tasks, onChangeTask, onDeleteTask}) { return (
    - {tasks.map(task => ( + {tasks.map((task) => (
  • - +
  • ))}
); } -function Task({ task, onChange, onDelete }) { +function Task({task, onChange, onDelete}) { const [isEditing, setIsEditing] = useState(false); let taskContent; if (isEditing) { @@ -622,24 +615,21 @@ function Task({ task, onChange, onDelete }) { <> { + onChange={(e) => { onChange({ ...task, - text: e.target.value + text: e.target.value, }); - }} /> - + }} + /> + ); } else { taskContent = ( <> {task.text} - + ); } @@ -648,26 +638,32 @@ function Task({ task, onChange, onDelete }) { { + onChange={(e) => { onChange({ ...task, - done: e.target.checked + done: e.target.checked, }); }} /> {taskContent} - + ); } ``` ```css -button { margin: 5px; } -li { list-style-type: none; } -ul, li { margin: 0; padding: 0; } +button { + margin: 5px; +} +li { + list-style-type: none; +} +ul, +li { + margin: 0; + padding: 0; +} ```
@@ -677,16 +673,13 @@ If you want, you can even move the reducer to a different file: ```js App.js -import { useReducer } from 'react'; +import {useReducer} from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import tasksReducer from './tasksReducer.js'; export default function TaskApp() { - const [tasks, dispatch] = useReducer( - tasksReducer, - initialTasks - ); + const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); function handleAddTask(text) { dispatch({ @@ -699,23 +692,21 @@ export default function TaskApp() { function handleChangeTask(task) { dispatch({ type: 'changed', - task: task + task: task, }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', - id: taskId + id: taskId, }); } return ( - <> + <>

Prague itinerary

- + { + return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { @@ -756,7 +747,7 @@ export default function tasksReducer( }); } case 'deleted': { - return tasks.filter(t => t.id !== action.id); + return tasks.filter((t) => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); @@ -766,50 +757,45 @@ export default function tasksReducer( ``` ```js AddTask.js hidden -import { useState } from 'react'; +import {useState} from 'react'; -export default function AddTask({ onAddTask }) { +export default function AddTask({onAddTask}) { const [text, setText] = useState(''); return ( <> setText(e.target.value)} + onChange={(e) => setText(e.target.value)} /> - + - ) + ); } ``` ```js TaskList.js hidden -import { useState } from 'react'; +import {useState} from 'react'; -export default function TaskList({ - tasks, - onChangeTask, - onDeleteTask -}) { +export default function TaskList({tasks, onChangeTask, onDeleteTask}) { return (
    - {tasks.map(task => ( + {tasks.map((task) => (
  • - +
  • ))}
); } -function Task({ task, onChange, onDelete }) { +function Task({task, onChange, onDelete}) { const [isEditing, setIsEditing] = useState(false); let taskContent; if (isEditing) { @@ -817,24 +803,21 @@ function Task({ task, onChange, onDelete }) { <> { + onChange={(e) => { onChange({ ...task, - text: e.target.value + text: e.target.value, }); - }} /> - + }} + /> + ); } else { taskContent = ( <> {task.text} - + ); } @@ -843,59 +826,65 @@ function Task({ task, onChange, onDelete }) { { + onChange={(e) => { onChange({ ...task, - done: e.target.checked + done: e.target.checked, }); }} /> {taskContent} - + ); } ``` ```css -button { margin: 5px; } -li { list-style-type: none; } -ul, li { margin: 0; padding: 0; } +button { + margin: 5px; +} +li { + list-style-type: none; +} +ul, +li { + margin: 0; + padding: 0; +} ```
-Component logic can be easier to read when you separate concerns like this. Now the event handlers only specify *what happened* by dispatching actions, and the reducer function determines *how the state updates* in response to them. +Component logic can be easier to read when you separate concerns like this. Now the event handlers only specify _what happened_ by dispatching actions, and the reducer function determines _how the state updates_ in response to them. -## Comparing `useState` and `useReducer` {/*comparing-usestate-and-usereducer*/} +## Comparing `useState` and `useReducer` {/* comparing-usestate-and-usereducer */} Reducers are not without downsides! Here's a few ways you can compare them: -* **Code size:** Generally, with `useState` you have to write less code upfront. With `useReducer`, you have to write both a reducer function _and_ dispatch actions. However, `useReducer` can help cut down on the code if many event handlers modify state in a similar way. -* **Readability:** `useState` is very easy to read when the state updates are simple. When they get more complex, they can bloat your component's code and make it difficult to scan. In this case, `useReducer` lets you cleanly separate the *how* of update logic from the *what happened* of event handlers. -* **Debugging:** When you have a bug with `useState`, it can be difficult to tell _where_ the state was set incorrectly, and _why_. With `useReducer`, you can add a console log into your reducer to see every state update, and _why_ it happened (due to which `action`). If each `action` is correct, you'll know that the mistake is in the reducer logic itself. However, you have to step through more code than with `useState`. -* **Testing:** A reducer is a pure function that doesn't depend on your component. This means that you can export and test it separately in isolation. While generally it's best to test components in a more realistic environment, for complex state update logic it can be useful to assert that your reducer returns a particular state for a particular initial state and action. -* **Personal preference:** Some people like reducers, others don't. That's okay. It's a matter of preference. You can always convert between `useState` and `useReducer` back and forth: they are equivalent! +- **Code size:** Generally, with `useState` you have to write less code upfront. With `useReducer`, you have to write both a reducer function _and_ dispatch actions. However, `useReducer` can help cut down on the code if many event handlers modify state in a similar way. +- **Readability:** `useState` is very easy to read when the state updates are simple. When they get more complex, they can bloat your component's code and make it difficult to scan. In this case, `useReducer` lets you cleanly separate the _how_ of update logic from the _what happened_ of event handlers. +- **Debugging:** When you have a bug with `useState`, it can be difficult to tell _where_ the state was set incorrectly, and _why_. With `useReducer`, you can add a console log into your reducer to see every state update, and _why_ it happened (due to which `action`). If each `action` is correct, you'll know that the mistake is in the reducer logic itself. However, you have to step through more code than with `useState`. +- **Testing:** A reducer is a pure function that doesn't depend on your component. This means that you can export and test it separately in isolation. While generally it's best to test components in a more realistic environment, for complex state update logic it can be useful to assert that your reducer returns a particular state for a particular initial state and action. +- **Personal preference:** Some people like reducers, others don't. That's okay. It's a matter of preference. You can always convert between `useState` and `useReducer` back and forth: they are equivalent! We recommend using a reducer if you often encounter bugs due to incorrect state updates in some component, and want to introduce more structure to its code. You don't have to use reducers for everything: feel free to mix and match! You can even `useState` and `useReducer` in the same component. -## Writing reducers well {/*writing-reducers-well*/} +## Writing reducers well {/* writing-reducers-well */} Keep these two tips in mind when writing reducers: -* **Reducers must be pure.** Similar to [state updater functions](/learn/queueing-a-series-of-state-updates), reducers run during rendering! (Actions are queued until the next render.) This means that reducers [must be pure](/learn/keeping-components-pure)—same inputs always result in the same output. They should not send requests, schedule timeouts, or perform any side effects (operations that impact things outside the component). They should update [objects](/learn/updating-objects-in-state) and [arrays](/learn/updating-arrays-in-state) without mutations. -* **Each action describes a single user interaction, even if that leads to multiple changes in the data.** For example, if a user presses "Reset" on a form with five fields managed by a reducer, it makes more sense to dispatch one `reset_form` action rather than five separate `set_field` actions. If you log every action in a reducer, that log should be clear enough for you to reconstruct what interactions or responses happened in what order. This helps with debugging! +- **Reducers must be pure.** Similar to [state updater functions](/learn/queueing-a-series-of-state-updates), reducers run during rendering! (Actions are queued until the next render.) This means that reducers [must be pure](/learn/keeping-components-pure)—same inputs always result in the same output. They should not send requests, schedule timeouts, or perform any side effects (operations that impact things outside the component). They should update [objects](/learn/updating-objects-in-state) and [arrays](/learn/updating-arrays-in-state) without mutations. +- **Each action describes a single user interaction, even if that leads to multiple changes in the data.** For example, if a user presses "Reset" on a form with five fields managed by a reducer, it makes more sense to dispatch one `reset_form` action rather than five separate `set_field` actions. If you log every action in a reducer, that log should be clear enough for you to reconstruct what interactions or responses happened in what order. This helps with debugging! -## Writing concise reducers with Immer {/*writing-concise-reducers-with-immer*/} +## Writing concise reducers with Immer {/* writing-concise-reducers-with-immer */} Just like with [updating objects](/learn/updating-objects-in-state#write-concise-update-logic-with-immer) and [arrays](/learn/updating-arrays-in-state#write-concise-update-logic-with-immer) in regular state, you can use the Immer library to make reducers more concise. Here, [`useImmerReducer`](https://github.com/immerjs/use-immer#useimmerreducer) lets you mutate the state with `push` or `arr[i] =` assignment: ```js App.js -import { useImmerReducer } from 'use-immer'; +import {useImmerReducer} from 'use-immer'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; @@ -905,19 +894,17 @@ function tasksReducer(draft, action) { draft.push({ id: action.id, text: action.text, - done: false + done: false, }); break; } case 'changed': { - const index = draft.findIndex(t => - t.id === action.task.id - ); + 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); + return draft.filter((t) => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); @@ -926,10 +913,7 @@ function tasksReducer(draft, action) { } export default function TaskApp() { - const [tasks, dispatch] = useImmerReducer( - tasksReducer, - initialTasks - ); + const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks); function handleAddTask(text) { dispatch({ @@ -942,23 +926,21 @@ export default function TaskApp() { function handleChangeTask(task) { dispatch({ type: 'changed', - task: task + task: task, }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', - id: taskId + id: taskId, }); } return ( <>

Prague itinerary

- +