이전에는 useState
를 사용해서 상태를 업데이트 해주었는데, useReducer
hook을 사용해서 상태 업데이트도 가능하다.
useState
와 useReducer
리액트에서 컴포넌트의 상태 관리를 위해 기본적으로 가장 많이 쓰이는 hook은 useState
함수이다. 그러나 좀 더 복잡한 상태 관리가 필요한 리액트 컴포넌트에서는 useReducer
hook 함수를 사용할 수 있다.
useState
: 다음 상태를 직접 지정해주는 방식으로 상태 업데이트
useReducer
: action
이라는 객체를 기반으로 상태 업데이트, action
객체는 업데이트를 할 때 참조하는 객체
useReducer
를 사용하면 상태 업데이트 로직을 컴포넌트 밖으로 분리할 수 있다.
const [state, dispatch] = useReducer(reducer, initialArg, init);
const [<상태 객체>, <dispatch 함수>] = useReducer(<reducer 함수>, <초기 상태>, <초기 함수>)
reducer
함수는 현재 상태(state) 객체와 행동(action) 객체를 파라미터로 받아서 새로운 상태 객체를 반환하는 함수이다.
dispatch
함수는 컴포넌트 내에서 상태 변경을 일으키기 위해서 사용되는데 파라미터로reducer
함수에 넘길action
객체를 받는다.
action
객체는 관행적으로 어떤 부류의 행동인지를 나타내는 type 속성과 해당 행동과 관련된 데이터를 담고 있다.
다시 말해, 컴포넌트에서dispatch
함수에 행동(action)을 던지면, reducer 함수가 이 행동(action)에 따라서 상태(state)를 변경해준다.
reducer
는 현재 상태(state)와, 행동(action) 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수이다. reducer
에서 반환하는 상태는 곧 컴포넌트가 지닐 새로운 상태가 됩니다.
function reducer(state, action) {
// 새로운 상태를 만드는 로직
// const nextState = ...
return nextState;
}
useReducer()
hook 함수는 첫번째 파라미터로 넘어오는 reducer
함수를 통해 컴포넌트의 상태가 action
에 따라 어떻게 변해야하는지 정의한다. action
은 업데이트를 위한 정보를 가지고 있다.
// 카운터에 1을 더하는 액션
{
type: 'INCREMENT'
}
// 카운터에 1을 빼는 액션
{
type: 'DECREMENT'
}
// input 값을 바꾸는 액션
{
type: 'CHANGE_INPUT',
key: 'email',
value: 'tester@react.com'
}
// 새 할 일을 등록하는 액션
{
type: 'ADD_TODO',
todo: {
id: 1,
text: 'useReducer 배우기',
done: false,
}
}
카운터 컴포넌트에서 사용할 reducer
함수는 switch 분기문을 이용하면 이해하기 쉽게 작성 가능하다.
import React, { useReducer } from "react";
// 1) reducer 함수 만들기
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
throw new Error("Unhandled action");
}
}
function Counter() {
// 2) useReducer hook 사용
const [number, dispatch] = useReducer(reducer, 0);
const onIncrease = () => {
dispatch({
type: "INCREMENT",
});
};
const onDecrease = () => {
dispatch({
type: "DECREMENT",
});
};
return (
<div>
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
}
export default Counter;
import React, { useReducer, useRef, useMemo, useCallback } from "react";
import CreateUser from "./CreateUser";
import UserList from "./UserList";
// useMemo: 특정값이 바뀌었을 때만 특정 함수를 실행해서 연산하도록 처리
function countActiveUsers(users) {
console.log("활성 사용자 수 세는 중");
return users.filter((user) => user.active).length;
// active가 true인 사용자 필터링해 수를 연산해서 가져옴
}
// 1) 앱 컴포넌트에서 사용할 초기상태를 컴포넌트 바깥에 선언해주기
const initialState = {
inputs: {
username: "",
email: "",
},
users: [
{
id: 1,
username: "su",
email: "susu@gmail.com",
active: true,
},
{
id: 2,
username: "liz",
email: "lili@gmail.com",
active: false,
},
{
id: 3,
username: "ro",
email: "ro@gmail.com",
active: false,
},
],
};
// 2) reducer 함수의 틀 만들기
function reducer(state, action) {
switch (action.type) {
case "CHANGE_INPUT":
return {
...state, // why? 새로운 상태 만들 때 불변성을 지켜주기 위해 state(users) 한번 복사
inputs: {
...state.inputs, // 마찬가지로 기존 state.inputs 한번 복사
[action.name]: action.value,
},
};
case "CREATE_USER":
return {
inputs: initialState.inputs, // input 공백값 반환
users: state.users.concat(action.user),
};
case "TOGGLE_USER":
return {
...state,
users: state.users.map((user) =>
user.id === action.id
? {
...user,
active: !user.active,
}
: user
),
}; //
case "REMOVE_USER":
return {
...state, // 복사
users: state.users.filter((user) => user.id !== action.id),
};
default:
return state;
}
}
function App() {
// 3) 내부에서 useReducer 함수 선언
const [state, dispatch] = useReducer(reducer, initialState);
// 6-1) useRef 사용 아이디값 부여
const nextId = useRef(4);
// 4) 비구조화 할당으로 users, inputs 내용 추출
const { users } = state;
const { username, email } = state.inputs;
// 5) onChange 함수 구현
const onChange = useCallback((e) => {
const { name, value } = e.target;
dispatch({ type: "CHANGE_INPUT", name, value });
}, []);
// 6) onCreate 구현
const onCreate = useCallback(() => {
dispatch({
type: "CREATE_USER",
user: {
id: nextId.current,
username,
email,
},
});
nextId.current += 1;
}, [username, email]); // 함수에서 사용하는 상태 혹은 props, deps 배열에 포함
// 7) onToggle
const onToggle = useCallback((id) => {
dispatch({
type: "TOGGLE_USER",
id,
});
}, []);
// 8) onRemove
const onRemove = useCallback((id) => {
dispatch({
type: "REMOVE_USER",
id, // id값만 비교해주니까!
});
}, []);
// 9) 활성사용자 수
const count = useMemo(() => countActiveUsers(users), [users]);
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} onRemove={onRemove} onToggle={onToggle} />
<div>활성 사용자 수 : {count} </div>
</>
);
}
export default App;