Spec-Zone.ru › React

Масштабирование с помощью редюсера и контекста

Редюсеры позволяют объединить логику обновления состояния компонента. Контекст позволяет передавать информацию глубоко вложенным компонентам. Вы можете объединить редюсеры и контекст, чтобы управлять состоянием сложного экрана.

Вы узнаете

  • Как объединить редюсер с контекстом
  • Как избежать передачи состояния и dispatch через пропсы
  • Как хранить контекст и логику состояния в отдельном файле

Объединение редюсера с контекстом

В этом примере из введения в редюсеры состоянием управляет редюсер. Функция редюсера содержит всю логику обновления состояния и объявлена внизу этого файла:

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. Чтобы другие компоненты могли читать список задач или изменять его, нужно явно передавать вниз текущее состояние и обработчики событий, которые его изменяют, в качестве пропсов.

Например, TaskApp передаёт список задач и обработчики событий компоненту TaskList:

<TaskList
  tasks={tasks}
  onChangeTask={handleChangeTask}
  onDeleteTask={handleDeleteTask}
/>

А TaskList передаёт обработчики событий компоненту Task:

<Task
  task={task}
  onChange={onChangeTask}
  onDelete={onDeleteTask}
/>

В небольшом примере, таком как этот, такой подход работает хорошо, но если между компонентами находятся десятки или сотни других компонентов, передавать через них всё состояние и функции может быть очень утомительно!

Поэтому, вместо того чтобы передавать их через пропсы, можно поместить в контекст и состояние tasks, и функцию dispatch. Это позволит любому компоненту в дереве ниже TaskApp читать список задач и отправлять действия без повторяющегося «протаскивания пропсов».

Вот как можно объединить редюсер с контекстом:

  1. Создайте контекст.
  2. Поместите состояние и dispatch в контекст.
  3. Используйте контекст в любом месте дерева.

Шаг 1. Создайте контекст

Хук useReducer возвращает текущее tasks и функцию dispatch, которая позволяет обновлять их:

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

Чтобы передать их вниз по дереву, создайте два отдельных контекста:

  • TasksContext предоставляет текущий список задач.
  • TasksDispatchContext предоставляет функцию, с помощью которой компоненты могут отправлять действия.

Экспортируйте их из отдельного файла, чтобы позже импортировать в другие файлы:

import { createContext } from 'react';

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

Здесь вы передаёте null в качестве значения по умолчанию для обоих контекстов. Фактические значения предоставит компонент TaskApp.

Шаг 2. Поместите состояние и dispatch в контекст

Теперь можно импортировать оба контекста в компонент TaskApp. Возьмите tasks и dispatch, возвращаемые функцией useReducer(), и предоставьте их всему дереву ниже:

import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
  // ...
  return (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>
        ...
      </TasksDispatchContext>
    </TasksContext>
  );
}

Пока что вы передаёте информацию и через пропсы, и через контекст:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksContext, TasksDispatchContext } from './TasksContext.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 (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>
        <h1>Day off in Kyoto</h1>
        <AddTask
          onAddTask={handleAddTask}
        />
        <TaskList
          tasks={tasks}
          onChangeTask={handleChangeTask}
          onDeleteTask={handleDeleteTask}
        />
      </TasksDispatchContext>
    </TasksContext>
  );
}

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 }
];

На следующем шаге вы уберёте передачу через пропсы.

Шаг 3. Используйте контекст в любом месте дерева

Теперь не нужно передавать список задач или обработчики событий вниз по дереву:

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

Вместо этого любой компонент, которому нужен список задач, может получить его из TasksContext:

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

Чтобы обновить список задач, любой компонент может получить из контекста функцию dispatch и вызвать её:

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>
    // ...

Компонент TaskApp не передаёт обработчики событий вниз, а TaskList также не передаёт обработчики событий компоненту Task. Каждый компонент читает нужный ему контекст:

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>
  );
}

Состояние по-прежнему «находится» в компоненте верхнего уровня TaskApp, где им управляет useReducer. Но теперь его tasks и dispatch доступны каждому компоненту ниже по дереву: достаточно импортировать эти контексты и использовать их.

Перенос всей связующей логики в один файл

Это необязательно, но можно ещё больше упростить компоненты, переместив редюсер и контекст в один файл. Сейчас TasksContext.js содержит только два объявления контекстов:

import { createContext } from 'react';

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

Этот файл скоро станет слишком большим! Вы переместите редюсер в тот же файл. Затем там же объявите новый компонент TasksProvider. Этот компонент объединит все части:

  1. Он будет управлять состоянием с помощью редюсера.
  2. Он предоставит оба контекста компонентам ниже.
  3. Он будет принимать children в качестве пропса, чтобы вы могли передавать ему JSX.
export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

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

Это избавит компонент TaskApp от всей сложности и связующей логики:

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>
  );
}

Также можно экспортировать функции, которые используют контекст из TasksContext.js:

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

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

Когда компоненту нужно прочитать контекст, он может сделать это с помощью этих функций:

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

Это никак не меняет поведение, но позволяет позднее разделить эти контексты ещё больше или добавить логику в эти функции. Теперь вся связующая логика контекста и редюсера находится в TasksContext.js. Благодаря этому компоненты остаются чистыми и простыми, сосредоточенными на том, что они отображают, а не на том, откуда получают данные:

import { useState } from 'react';
import { useTasks, useTasksDispatch } from './TasksContext.js';

export default function TaskList() {
  const tasks = useTasks();
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <Task task={task} />
        </li>
      ))}
    </ul>
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useTasksDispatch();
  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>
  );
}

Можно считать, что TasksProvider — это часть экрана, которая умеет работать с задачами, useTasks — способ читать их, а useTasksDispatch — способ обновлять их из любого компонента ниже по дереву.

Примечание

Функции, такие как useTasks и useTasksDispatch, называются пользовательскими хуками. Функция считается пользовательским хуком, если её имя начинается с use. Благодаря этому внутри неё можно использовать другие хуки, например useContext.

По мере роста приложения у вас может появиться много подобных пар «контекст — редюсер». Это эффективный способ масштабировать приложение и поднимать состояние выше, не прилагая больших усилий каждый раз, когда нужно получить доступ к данным глубоко в дереве.

Итоги

  • Можно объединить редюсер с контекстом, чтобы любой компонент мог читать состояние и обновлять его.
  • Чтобы предоставить состояние и функцию dispatch компонентам ниже:
    1. Создайте два контекста (для состояния и для функций dispatch).
    2. Предоставьте оба контекста из компонента, который использует редюсер.
    3. Используйте нужный контекст в компонентах, которым нужно его читать.
  • Можно ещё больше упростить компоненты, переместив всю связующую логику в один файл.
    • Можно экспортировать компонент, например TasksProvider, который предоставляет контекст.
    • Также можно экспортировать пользовательские хуки, например useTasks и useTasksDispatch, чтобы читать его.
  • В приложении может быть много подобных пар «контекст — редюсер».

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/scaling-up-with-reducer-and-context

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API