Управление состоянием с помощью React

TL;DR

Это руководство представляет собой сборник многих видео и статей на YouTube, которые я смотрел и читал об управлении состоянием с помощью ReactJS.

Управление состоянием имеет важное значение в приложениях React, потому что наши проекты строятся на нашем выборе того, как с ними обращаться.

Состояние приложения можно разделить на состояние пользовательского интерфейса и состояние сервера (кэш). Они очень разные; поэтому мы не должны использовать одни и те же инструменты и методы для обработки каждого из них.

Использование библиотеки управления состоянием сервера для обработки запросов и мутаций, таких как React Query или SWR, устранит большую часть сложности вашего кода, оставив лишь несколько состояний пользовательского интерфейса, таких как открытые и закрытые модальные окна, для управления встроенными хуками из React и другие техники.

Государственное управление

Существуют различные категории состояния, но каждый тип состояния может относиться к одной из двух категорий:

- Кэш сервера — состояние, которое фактически хранится на сервере и хранится в клиенте для быстрого доступа (например, данные пользователя).

- Состояние пользовательского интерфейса — состояние, которое полезно только в пользовательском интерфейсе для управления интерактивными частями нашего приложения (например, модальное состояние isOpen).

Мы совершаем ошибку, когда объединяем эти два понятия. Кэш сервера по своей сути имеет проблемы, отличные от состояния пользовательского интерфейса, и им нужно управлять по-другому. Если вы поймете, что то, что у вас есть, на самом деле не является состоянием, а являетсякэшемсостояний, тогда вы сможете правильно управлять им.
Кент С. Доддс, управление состоянием приложения с помощью React.

Управление состоянием пользовательского интерфейса

React является библиотекой управления состоянием пользовательского интерфейса, и лучшим подходом к управлению состоянием пользовательского интерфейса является использование встроенного хука useState.

1. useState (рекомендуется)

Вот пример счетчика, демонстрирующий некоторые варианты обработки состояния пользовательского интерфейса с помощью React.

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return <button onClick={increment}>{count}</button>
}
function App() {
  return (
    <div>
      <Counter />
    </div>
  )
}

Совместное использование

Это распространенная проблема, с которой сталкиваются разработчики при создании приложений React. Совместное использование состояния между компонентами не всегда просто, потому что компоненты часто слишком сложны.

Но допустим, ваш менеджер по продукту хочет, чтобы вы отображали текущее значение счетчика для пользователя.

function CountDisplay() {
  // Where do I get the {count} value?
  return <p>The current counter value is {count}</p>
}
function App() {
  return (
    <div>
      <CountDisplay />
      <Counter />
    </div>
  )
}

На данный момент нет необходимости в какой-либо библиотеке управления состоянием, мы можем делиться состоянием между компонентами через реквизиты, подняв состояние до общего родителя Counter и CountDisplay (в нашем случае это компонент App ).

Состояние подъема

Это может показаться слишком простым, но в реальном мире трудно определить эти возможности, потому что наши компоненты не всегда так просты, как этот.

Здесь мы перемещаем состояние из компонента Counter в компонент App и делимся им через свойства.

function CountDisplay({ count }) {
  return <p>The current counter count is **{count}**</p>
}
function Counter({ count, onIncrement }) {
  return <button onClick={onIncrement}>**{count}**</button>
}
function App() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <CountDisplay />
      <Counter count={count} onIncrement={increment} />
    </div>
  )
}

Чтобы узнать больше о том, где расположить ваше состояние, хорошо начать с Thinking in React.

бурение опор

Поднятие состояния — это хорошая техника для совместного использования состояния с компонентами, но когда вам нужно поделиться чем-то из компонента очень высокого уровня в дереве компонентов, использование этой техники введет нечто, называемое Детализация реквизита.

Например, представьте, что наш компонент CountDisplay по какой-то причине стал слишком сложным, и мы решили переместить сообщение счетчика в дочерний компонент CountMessage.

function CountMessage({ count }) {
  return <p>The current counter count is {count}</p>
}
function CountDisplay({ count }) {
  // ...very complex logic
  return <div><CountMessage count={count} /></div>
}
function App() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <CountDisplay />
      <Counter count={count} onIncrement={increment} />
    </div>
  )
}

Сверление опор не обязательно плохо, но использование Context API для решения этой проблемы не рекомендуется.

Вместо этого мы можем использовать композиции компонентов, чтобы избежать этой проблемы.

Компонентный состав

Вот пример, который демонстрирует, как мы можем избежать сверления реквизита, используя композицию:

function CountMessage({ count }) {
  return <p>The current counter count is {count}</p>
}
function CountDisplay({ message }) {
  // ...very complex logic
  return <div>{message}</div>
}
function App() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <CountDisplay message={<CountMessage count={count} />} />
      <Counter count={count} onIncrement={increment} />
    </div>
  )
}

Передача компонента CountMessage в качестве свойства позволяет нам поделиться состоянием count всего на один уровень вниз по дереву компонентов и убрать сверление опор.

У Майкла Джексона, сооснователя remix_run, есть замечательное видео на YouTube о Использовании композиции в React, чтобы избежать «проп-бурения».

@mjackson:
Непопулярное мнение: весь этот мусор, который вы добавляете в контекст React, должен быть просто реквизитом. Допустимое использование контекста встречается редко, в основном для кода библиотеки, а не для вашего приложения.

https://twitter.com/mjackson/status/1195431511417208834

2. useContext (не рекомендуется)

Перед использованием контекста

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

Если вы хотите избежать прохождения некоторых реквизитов через множество уровней, компонентная композиция зачастую является более простым решением, чем контекст.

https://reactjs.org/docs/context.html#before-you-use-context

Почему Context API не так хорош?

Context API является очень мощным, но иногда мы используем его не в тех случаях.

При его использовании мы должны помнить о нескольких вещах:

  • Потребители привязаны к своим поставщикам. Это означает, что вы не можете повторно использовать потребителей (компоненты, которые обращаются к значению контекста), если поставщик недоступен.
  • Когда значение поставщика изменится, все его потребители будут перерисованы. Это может привести к непреднамеренным проблемам с производительностью.
  • Используйте его, когда значение вашего поставщика не меняется слишком часто, и вы не возражаете против повторного рендеринга ваших потребителей.

3. Другие решения для управления состоянием пользовательского интерфейса

Если хука React useState недостаточно для обработки состояния пользовательского интерфейса в вашем приложении, нам нужно знать, почему этого недостаточно, а затем начать искать другие решения для этого варианта использования.

Jotai и Zustand в настоящее время являются хорошими вариантами, но они решают разные проблемы.

Когда использовать какой:

- Если вам нужна замена useState+useContext, Jotai подойдет.

- Если вы хотите обновить состояние вне React, Zustand работает лучше.

- Если разделение кода важно, Jotai должен работать хорошо.

- Если вы предпочитаете инструменты разработки Redux, вам подойдет Zustand.

- Если вы хотите использовать Suspense, Jotai — это то, что вам нужно.

https://github.com/pmndrs/jotai/issues/13

Recoil – это альтернатива Jotai.

Если вы хотите узнать больше, послушайте, как Тео расскажет о разнице между Jotai и Zustand и о том, чем эти альтернативы отличаются от Redux.

Управление состоянием сервера

Некоторые библиотеки очень хорошо обрабатывают состояния сервера и предоставляют вам состояния загрузки, помощники по запросам и мутациям, оптимистичные обновления с откатами и многое другое:

  1. Реагировать на запрос;
  2. КСВ;
  3. Клиент Apollo, только для тех, кто использует GraphQL;

Я недостаточно использовал эти три, чтобы сказать вам, какой из них лучше, но вот мои соображения о React Query и SWR:

Реагировать на запрос

  • Библиотека с отличными утилитами для выборки данных, мутации, оптимистичных обновлений и многого другого.
  • Встроенные инструменты разработчика
  • Требуется поставщик кэша
  • Его использует больше людей, а значит больше примеров на StackOverflow.

Основной пример

const queryClient = new QueryClient()
function Component() {
  const query = useQuery(['todos'], getTodos);
  return (
    // needs a Cache Provider
    <QueryClientProvider client={queryClient}>
      <TodoList posts={query.data} />
    </QueryClientProvider>
  )
}
function AddTodo() {
  const mutation = useMutation(newTodo => axios.post('/todos', newTodo), {
    onSuccess: () => {
      // needs explicit invalidation
      queryClient.invalidateQueries(['todos']);
    }
  });
  return (
    <button
      onClick={() => {
        mutation.mutate({ id: new Date(), title: 'Do Laundry' })
      }}
    >
      Create Todo
    </button>
  );
}

КСВ

  • Облегченная библиотека (9,9 КБ в уменьшенном виде более 43,1 КБ в уменьшенном виде из response-query) с потрясающими утилитами для выборки данных, изменения, оптимистичных обновлений и многого другого.
  • Инструменты разработчика с внешней библиотекой
  • Ему не нужен поставщик кэша
  • ИМО имеет более простую реализацию

Основной пример

function Component() {
  const query = useSWR('todos', getTodos);
  return (
    // no Cache Provider or query client
    <TodoList posts={query.data} />
  )
}
function AddTodo() {
  const { mutate } = useSWRConfig();
  // Automatically invalidates queries by key
  const addTodo = (newTodo) => mutate('todos', () => {
    axios.post('/todos', newTodo);
  });
  return (
    <button
      onClick={() => {
        addTodo({ id: new Date(), title: 'Do Laundry' })
      }}
    >
      Create Todo
    </button>
  );
}

Заключение

Предположим, мы объединяем библиотеку управления состоянием сервера с методами, упомянутыми в этой статье, для управления состоянием пользовательского интерфейса. В этом случае мы избавились бы от всех хлопот, связанных с получением данных на стороне клиента, при этом используя передовой опыт для обмена возвращаемыми данными между компонентами.

Наши проекты React станут проще поддерживать и внедрять новые функции.