Overview.
- Reducer와 Context를 함께 사용해 복잡한 화면의 State를 관리할 수 있음
- Reducer는 컴포넌트의 State 업데이트 로직을 통합할 수 있음
- Context는 다른 컴포넌트들에 정보를 전달할 수 있음
Reducer + Context
- Reducer로 아래와 같이 tasks를 핸들링하는 업데이트 로직을 통합 관리할 수 있었음
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
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>Day off in Kyoto</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
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);
}
}
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Philosopher’s Path', done: true },
{ id: 1, text: 'Visit the temple', done: false },
{ id: 2, text: 'Drink matcha', done: false }
];
- 그러나 현재
tasks
와 dispatch
는 최상위 컴포넌트인 <TaskApp>
에서만 쓸 수 있음
- 다른 컴포넌트가 읽어나 변경하려면 여기서는 Prop Driliing 해야만 함
- 따라서 Context를 사용할 시점임
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
- 트리를 통해 전달하기 위해, State와 Action에 해당하는 Context를 따로 생성함
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
- 위와 같이 Provider에
tasks
와 dispatch
를 넘겨줘 트리 전체에 제공함
import { useState, useContext } from 'react';
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskList() {
const tasks = useContext(TasksContext);
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task task={task} />
</li>
))}
</ul>
);
}
function Task({ task }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useContext(TasksDispatchContext);
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
dispatch({
type: 'changed',
task: {
...task,
text: e.target.value
}
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
taskContent = (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={e => {
dispatch({
type: 'changed',
task: {
...task,
done: e.target.checked
}
});
}}
/>
{taskContent}
<button onClick={() => {
dispatch({
type: 'deleted',
id: task.id
});
}}>
Delete
</button>
</label>
);
}
- 그리고 하위 컴포넌트들은 필요에 의해
useContext
를 사용해 State와 Action을 취하면 됨
import { createContext, useReducer } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
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);
}
}
}
const initialTasks = [
{ id: 0, text: 'Philosopher’s Path', done: true },
{ id: 1, text: 'Visit the temple', done: false },
{ id: 2, text: 'Drink matcha', done: false }
];
- Reducer와 Context를 모두 하나의 파일에 적성해 컴포넌트들을 더 정리해 볼 수 있음
- Reducer를 같은 파일로 옮기고
<TasksProvider>
컴포넌트를 선언했음
- Reducer로 State를 관리
- 두 Context를 모두 하위 컴포넌트에 제공
Result.
- Reducer와 Context 조합으로, 자식 컴포넌트가 상위에 적용된 State와 Action을 읽고 쓸 수 있게함
- State와 Action 제공을 위해 각각의 Context를 생성함
- 하나의 파일로 합쳐 컴포넌트를 정리할 수도 있음
- Context를 제공하는
TasksProvider
같은 컴포넌트 내보낼 수 있음
- State와 Action을 바로 사용하도록
useTasks
와 useTasksDispatch
같은 사용자 정의 Hook을 내보낼 수 있음
- 이를
context-reducer
라고 함
References.