useReducer는 컴포넌트에 reducer를 더해주는 훅이다.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
useReducer(reducer, initialArg, init?)
Reference
형태
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
Parameters
reducer
: state가 업데이트 되는 방식을 명시한 함수. 순수 함수여야 하고 state와 action을 인자로 받아 다음 state를 반환해야 한다.initialArg
: 초기 state 값. init 함수에 따라 계산된다.init(optional)
: 초기 state를 반환해야 하는 initializer 함수. init을 쓰지 않으면 초기 state 값은 initialArg로 설정되고, init을 쓰면 init(initialArg)로 설정된다.Returns
useReducer는 두 개의 값을 가진 배열을 반환한다.
1. current state
: 현재 state 값. 첫 번째 렌더링되는 경우 init(initialArg) 혹은 initialArg으로 설정된다.
2. dispatch function
: state를 업데이트해주는 함수. 리렌더링을 발생시킨다.
주의사항
dispatch function
useReducer에 의해 반환되는 dispatch 함수는 state를 다른 값으로 업데이트할 수 있게 해주고 리렌더링을 발생시킨다.
dispatch 함수에는 action만 넘겨주면 된다.
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
dispatch로 넘긴 action과 현재 state를 바탕으로 React가 reducer 함수를 실행해서 그 결과값을 다음 state로 설정한다.
Parameters
action
: 유저의 동작을 의미한다.(add, delete, update,,)Returns
dispatch 함수는 결과값을 반환하지 않는다.
주의사항
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
값을 업데이트하고 싶을 때는 dispatch 함수를 이용한다.
function handleClick() {
dispatch({ type: 'incremented_age' });
}
useReducer는 useState와 매우 유사하지만 state 업데이트 로직을 이벤트 핸들러로부터 분리해 컴포넌트 외부로 보낸다는 차이점이 있다.
(useState와 useReducer를 비교한 부분은 맨 아래 부분 참조)
function reducer(state, action) {
// ...
}
각 action type별 처리 로직은 switch 문으로 작성하는게 컨벤션이다.
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);
}
action은 어떤 형태로든 쓸 수 있지만 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는 read only이므로 직접 mutate하지 말고 replace하기
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
};
}
replace하는게 복잡할 때는 Immer
라이브러리를 사용해볼 수 있다.
아래처럼 되어 있으면 상대적으로 이해하기가 어려워지므로 이때 Immer 라이브러리를 사용하면 가독성을 개선할 수 있다.
case 'edited_message': {
return {
...state,
messages: {
...state.messages,
[state.selectedId]: action.message
}
};
}
사용 예제
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>
</>
);
}
배열 쓰는 경우
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 }
];
React는 initial state를 한 번만 저장하고 다음 렌더 시에는 무시한다.
아래 예제에서 createInitialState(username)는 첫 렌더링 시에 한 번만 쓰이지만 매 렌더링마다 호출된다.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...
이는 성능에 영향을 미칠 수 있기 때문에 initializer 함수를 세 번째 인자에 넣어주는게 좋다.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...
dispatch 함수를 실행해도 실행중인 코드의 state를 바로 변경하지 않는다.
새로운 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);
}
업데이트된 값을 보고 싶을 때는 reducer를 호출해서 확인하기
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
React는 다음 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;
}
// ...
}
}
mutate하지 말고 replace할 것.
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를 반환할 때 모든 case에서 기존 값을 복사하도록 하기.
아래에서 ...state를 빼먹으면 age 필드만 남고 나머지는 없어진다.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Don't forget this!
age: state.age + 1
};
}
// ...
state를 return하는 것을 까먹거나 매치되는 type이 없을 때 발생한다.
이유를 찾기 위해 switch문 마지막에 throw error를 추가할 것. (혹은 TypeScript를 이용해서 실수를 잡아낼 수 있음)
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}
코드 사이즈
: useState < useReducer가독성
: useState > useReducer디버깅
: useState < useReducer테스팅
: useState < useReducer개인적 선호도
: 개인 선호도에 따라 선택해도 상관없음.부정확한 state 업데이트로 인해 버그를 겪을 때 reducer를 쓰는 것을 권장한다.
모든 것에 reducer를 쓸 필요는 없고 섞어서 써도 된다.
1. reducer는 순수 함수여야 한다.
state 업데이트 함수와 비슷하게 렌더링 중에 실행되므로 순수 함수여야 한다.
2. action은 한 가지 동작만을 설명해야 한다.
만약에 5개의 필드를 관리하는 폼에서 reset을 누를 경우 5개의 개별 set_field
액션을 디스패치하기보다는 하나의 reset_form
액션을 디스패치하는게 더 합리적이다.
import { useState } from 'react';
export function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
// 아래가 보다 정확한 표현. 상태가 업데이트되는 시점에 최신 상태 값을 보장할 수 있음.
// 여러 dispatch 호출이 배치 처리되면서 상태 값이 불일치할 수 있는 문제를 방지할 수 있음.
function dispatch(action) {
setState((s) => reducer(s, action));
}
return [state, dispatch];
}
자료
React useReducer 공식 문서 : https://react.dev/reference/react/useReducer
https://react.dev/learn/extracting-state-logic-into-a-reducer#comparing-usestate-and-usereducer