Managing State-Scaling up with reducer and context

Yoseob Shin·2023년 3월 27일
0

react

목록 보기
6/6
post-thumbnail

Scaling up with reducer and context

Redux + react.js 같이 사용할려면 react native reducer + context 사용해 비슷한 개발 경험을 느낄수 있다. (차이점은 분명 있다. 예를 들어 Thunk나 Redux-saga같이 사용할수 있는 기능들을 리액트 라이브러리 자체에서 지원해 주지 않는 다는거...)

With this approach, a parent component with complex state manages it with a reducer. Other components anywhere deep in the tree can read its state via context. They can also dispatch actions to update that state.

A reducer helps keep the event handlers short and concise. However, as your app grows, you might run into another difficulty. Currently, the tasks state and the dispatch function are only available in the top-level TaskApp component.

In a small example like this, this works well, but if you have tens or hundreds of components in the middle, passing down all state and functions can be quite frustrating!

This is why, as an alternative to passing them through props, you might want to put both the tasks state and the dispatch function into context. This way, any component below TaskApp in the tree can read the tasks and dispatch actions without the repetitive “prop drilling”.

Step 1: Create the context

The useReducer Hook returns the current tasks and the dispatch function that lets you update them:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

To pass them down the tree, you will create two separate contexts:

  • TasksContext provides the current list of tasks.
  • TasksDispatchContext provides the function that lets components dispatch actions.
import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

Step 2: Put state and dispatch into context

Now you can import both contexts in your TaskApp component. Take the tasks and dispatch returned by useReducer() and provide them to the entire tree below:

Step 3: Use context anywhere in the tree

Now you don’t need to pass the list of tasks or the event handlers down the tree:

<TasksContext.Provider value={tasks}>
  <TasksDispatchContext.Provider value={dispatch}>
    <h1>Day off in Kyoto</h1>
    <AddTask />
    <TaskList />
  </TasksDispatchContext.Provider>
</TasksContext.Provider>

1) Instead, any component that needs the task list can read it from the TaskContext:

export default function TaskList() {
  const tasks = useContext(TasksContext);
  // ...

2) To update the task list, any component can read the dispatch function from context and call it:

export default function AddTask() {
  const [text, setText] = useState('');
  const dispatch = useContext(TasksDispatchContext);
  // ...
  return (
    // ...
    <button onClick={() => {
      setText('');
      dispatch({
        type: 'added',
        id: nextId++,
        text: text,
      });
    }}>Add</button>
    // ...
    

The TaskApp component does not pass any event handlers down, and the TaskList does not pass any event handlers to the Task component either. Each component reads the context that it needs.

The state still “lives” in the top-level TaskApp component, managed with useReducer. But its tasks and dispatch are now available to every component below in the tree by importing and using these contexts.

Moving all wiring into a single file

You can declutter the components by moving the reducer and context into a single file.

Currently, TasksContext.js contains only two context declarations:

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

This file is about to get crowded! You’ll move the reducer into that same file. Then you’ll declare a new TasksProvider component in the same file. This component will tie all the pieces together:

  1. It will manage the state with a reducer.
  2. It will provide both contexts to components below.
  3. It will take children as a prop so you can pass JSX to it.
// TasksContext.js
export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

// This removes all the complexity and wiring from your TaskApp component:

//  App.js

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

You can also export functions that use the context from TasksContext.js:

export function useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

// When a component needs to read context, it can do it through these functions:

const tasks = useTasks();
const dispatch = useTasksDispatch();

This doesn’t change the behavior in any way, but it lets you later split these contexts further or add some logic to these functions. Now all of the context and reducer wiring is in TasksContext.js. This keeps the components clean and uncluttered, focused on what they display rather than where they get the data:

You can think of TasksProvider as a part of the screen that knows how to deal with tasks, useTasks as a way to read them, and useTasksDispatch as a way to update them from any component below in the tree.

Functions like useTasks and useTasksDispatch are called Custom Hooks. Your function is considered a custom Hook if its name starts with use. This lets you use other Hooks, like useContext, inside it.

As your app grows, you may have many context-reducer pairs like this. This is a powerful way to scale your app and lift state up without too much work whenever you want to access the data deep in the tree.

Recap

  • You can combine reducer with context to let any component read and update state above it.
  • To provide state and the dispatch function to components below:
    1. Create two contexts (for state and for dispatch functions).
    2. Provide both contexts from the component that uses the reducer.
    3. Use either context from components that need to read them.
  • You can further declutter the components by moving all wiring into one file.
    1. You can export a component like TasksProvider that provides context.
    2. You can also export custom Hooks like useTasks and useTasksDispatch to read it.
  • You can have many context-reducer pairs like this in your app.
profile
coder for web development + noodler at programming synthesizers for sound design as hobbyist.

0개의 댓글