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.
the TaskApp component below holds an array of tasks in state and uses three different event handlers to add, remove, and edit tasks:
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
export default function TaskApp() {
const [tasks, setTasks] = useState(initialTasks);
function handleAddTask(text) {
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;
}
})
);
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.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},
];
To reduce this complexity and keep all your logic in one easy-to-access place, you can move that state logic into a single function outside your component, called a “reducer”.
Reducers are a different way to handle state. You can migrate from useState to useReducer in three steps:
1. Move from setting state to dispatching actions.
2. Write a reducer function.
3. Use the reducer from your component.
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.
먼저 리듀서를 사용하기 위해 위의 예시 코드에서 3가지 이벤트 핸들러에서 stateSetter 함수들을 빼고 dispatch 함수를 사용해 알맞은 action object를 패싱 하도록 하자. 리듀서를 사용할때는 사이드 이팩트를 주는 핸들러 로직에 stateSetter을 사용해 "무엇을 하라" 라고 리액트에 명령을 하는거보다 과거형으로 액션 개체를 리듀서 함수에 돌려 "유저가 무었을 했었다" 라고 표현하는게 맞는거 같다.
// 일반 핸들러:
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
// 리듀서를 사용할시 dispatch 함수와 액션 객체를 사용한 예시:
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
action 객체는 일반 자바스크립트 객체이다. 일반적으론 어떠한 이벤트가 발생했는지 최소한 미니멀 하게 서술할 값만 할당해 dispatch 함수와 사용한다.
An action object can have any shape.
The type is specific to a component, so in this example either 'added' or 'added_task' would be fine. Choose a name that says what happened!
Array.reduce((accumulator, currentValue) => {}, initialValue) 처럼 두개의 React의 reducer 함수도 두가지 인자를 받는다.(Redux도 똑같다)
중요한 key concept은 The reduce() operation lets you take an array and “accumulate” a single value out of many:
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5
The function you pass to reduce is known as a “reducer”. It takes the result so far and the current item, then it returns the next result. React reducers are an example of the same idea: they take the state so far and the action, and return the next state. In this way, they accumulate actions over time into state.
You could even use the reduce() method with an initialState and an array of actions to calculate the final state by passing your reducer function to it:
// index.js
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'},
];
let finalState = actions.reduce(tasksReducer, initialState);
// tasksReducer.js
export default function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
.....
}
자 다시 돌아와서:
React will set the state to what you return from the reducer.
To move your state setting logic from your event handlers to a reducer function in this example, you will:
function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
} else if(....).....
.....
// 위처럼 if 조건도 가능하지만 난 개인적으로 올드스쿨로 switch가 편하다 - 가독성이 편하기도 하고
function tasksReducer(tasks, action) {
switch(action.type) {
case 'added':{
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
}
]
};
case 'edited': {
..........
break;
}
.....
..
default: {
throw Error(`Unknown action: + ${action.type}`);
break;
}
}
We recommend wrapping each case block into the { and } curly braces so that variables declared inside of different cases don’t clash with each other. Also, a case should usually end with a return. or break If you forget to return, the code will “fall through” to the next case, which can lead to mistakes!
Finally, you need to hook up the tasksReducer to your component. Import the useReducer Hook from React:
import {useReducer} from 'react';
// Then you can replace useState:
const [tasks, setTasks] = useState(initialTasks); < -- X
// with useReducer like so:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); <-- O
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.
나중엔 reducer 함수를 module로 import해서 separate concerns 전략을 사용해 가독성이 보기좋은 코드를 작성 할수 있다.
Reducers are not without downsides! Here’s a few ways you can compare them:
We recommend using a reducer 1)if you often encounter bugs due to incorrect state updates in some component, and 2) 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.
Keep these two tips in mind when writing reducers:
They should not send requests, schedule timeouts, or perform any side effects (operations that impact things outside the component). They should update objects and arrays without mutations.