React 공부할끄니까! context-reducer로 State와 Dispatch를 한 곳에서 관리하는 방법?!

React 공부할끄니까! context-reducer로 State와 Dispatch를 한 곳에서 관리하는 방법?!

·

4 min read

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 }
];
  • 그러나 현재 tasksdispatch는 최상위 컴포넌트인 <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에 tasksdispatch를 넘겨줘 트리 전체에 제공함
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을 바로 사용하도록 useTasksuseTasksDispatch 같은 사용자 정의 Hook을 내보낼 수 있음
  • 이를 context-reducer라고 함

References.