useReducer
- React에서 상태를 관리할 때 사용하는 Hook
- 상태를 업데이트하는 로직을 분리하여 컴포넌트 외부에서 정의할 수 있게 해주는 함수
- 복잡한 상태 로직을 관리하거나, 여러 값들이 같은 액션에 따라 변경되는 경우 사용
const [state, dispatch] = useReducer(reducer, initialState, init);
reducer
: state를 업데이트 하는 역할
initialState
: reducer의 초기 상태 값
init
(선택) : 초기 상태 값을 만드는 함수, 초기 상태 값을 지연 생성할 수 있음
dispatch
: 액션을 발생시키는 함수, 정의된 액션을 reducer에 전달하여 상태를 업데이트할 수 있음 dispatch({ type: 'ACTION_NAME', payload: value })
예시 코드
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return {count: state.count + 1};
case 'DECREMENT':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, {count: 0});
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'INCREMENT'})}>+</button>
<button onClick={() => dispatch({type: 'DECREMENT'})}>-</button>
</>
);
}
- 'INCREMENT'와 'DECREMENT' 액션을 dispatch하여 count 값을 증가시키거나 감소시킵니다.
주의할 점
1. action type은 일관되게 관리
- 일반적으로 대문자로 작성하고 문자열 상수로 관리하는 것이 좋음
const ACTION_TYPES = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
};
function reducer(state, action) {
switch (action.type) {
case ACTION_TYPES.INCREMENT:
}
}
2. 상태 변경 로직은 불변성을 유지
- React의 상태는 불변성을 유지해야 함
- 상태를 직접 수정하지 말고 새로운 상태 객체를 생성해서 반환해야 한다.
3. dispatch는 비동기적으로 동작
dispatch
함수는 비동기적으로 동작
- 호출한 직후에는 상태가 바로 업데이트 되지 않을 수 있어서 상태의 최신 값을 확인하려면 호출 후에 컴포넌트의 다음 렌더링에서 확인해야 함
4. useReducer는 글로벌 상태 관리에도 사용될 수 있지만, 복잡성을 더해줄 수 있음
- 글로벌 상태 관리를 위해 많이 사용하지만, 많은 상태를 관리하거나 액션의 종류가 많은 경우
Redux
나 MobX
와 같은 상태 관리 라이브러리를 사용하는 것이 더 효율적일 수 있음
응용
- useReducer를 사용한 출석부 관리 시스템
- 학생을 추가하거나 삭제할 수 있고 출석이 완료 된 학생 이름을 클릭 시 줄을 긋는다.
import React, { useReducer, useState } from 'react';
import Student from './Student';
const initialState = {
count: 0,
students: []
}
const reducer = (state, action) => {
switch (action.type) {
case 'add':
const user = {
id: new Date().getTime(),
name: action.payload,
isComplete: false
}
return {
count: state.count+1,
students: [...state.students, user]
};
case 'complete':
return {
count: state.count,
students: state.students.map(e => (e.id === action.payload ? {...e, isComplete: !e.isComplete} : e))
}
case 'delete':
return {
count: state.count-1,
students: state.students.filter(e => e.id !== action.payload)
}
default:
return state.students;
}
}
const Attendance = () => {
const [name, setName] = useState('');
const [studentList, dispatch] = useReducer(reducer, initialState);
const addItem = () => {
dispatch({type: 'add', payload: name});
setName('');
}
return (
<div>
<h1>출석부</h1>
<p>총 학생 수 : {studentList.count}</p>
<div>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={addItem}>추가</button>
</div>
<ul>
{studentList.students.map(item => (
<Student key={item.id} name={item.name} id={item.id} isComplete={item.isComplete} dispatch={dispatch}/>
))}
</ul>
</div>
);
};
export default Attendance;
import React from 'react';
const Student = ({name, id, isComplete, dispatch}) => {
return (
<li>
<span onClick={() => dispatch({type: 'complete', payload: id})} style={{ textDecoration: isComplete ? "line-through" : "none" }}>{name}</span>
<button onClick={() => dispatch({type: 'delete', payload: id})}>삭제</button>
</li>
);
};
export default Student;
참고
별코딩 - useReducer