TL; DR: Крючки - это здорово, но нет.

С тех пор, как был представлен React hooks API, возникло много вопросов о том, заменят ли React хуки Redux.

На мой взгляд, между хуками и Redux мало общего. Хуки не давали нам волшебных новых возможностей состояния. Вместо этого он улучшил API для того, что мы уже могли делать с React. Однако API хуков сделал собственный API состояния React намного более удобным, и, поскольку он проще, чем модель class, которую он заменяет, я использую состояние компонента гораздо чаще, чем раньше, когда это было необходимо.

Чтобы понять, что я имею в виду, нам нужно лучше понять, почему мы вообще можем рассматривать Redux.

Что такое Redux?

Redux - это предсказуемая библиотека управления состоянием и архитектура, которая легко интегрируется с React.

Основные преимущества Redux:

  • Разрешение детерминированного состояния (включение детерминированного рендеринга в сочетании с чистыми компонентами).
  • Состояние транзакции.
  • Изолировать управление состоянием от операций ввода-вывода и побочных эффектов.
  • Единый источник достоверной информации о состоянии приложения.
  • Легко делитесь состоянием между различными компонентами.
  • Телеметрия транзакций (объекты действий с автоматическим протоколированием).
  • Отладка путешествия во времени.

Другими словами, Redux дает вам суперспособности по организации кода и отладке. Это упрощает создание кода, более удобного в сопровождении, и значительно упрощает отслеживание основной причины, когда что-то идет не так.

Что такое React Hooks?

Перехватчики React позволяют использовать функции жизненного цикла состояния и React без использования class и методов жизненного цикла компонентов React. Они были представлены в React 16.8.

Основные преимущества перехватчиков React:

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

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

С такими инструментами, как react-redux hooks API и React's useReducer hook, нет необходимости выбирать один из них. Используйте оба. Смешивать и сочетать.

Что заменяют крючки?

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

Что не заменяют крючки?

Я до сих пор часто использую:

  • Redux по всем перечисленным выше причинам.
  • Компоненты высшего порядка для решения сквозных задач, которые разделяются всеми или большинством моих представлений приложения, например поставщиком Redux, поставщиком общего макета, поставщиком конфигурации, аутентификацией / авторизацией, i18n, и так далее.
  • Разделение компонентов контейнера и дисплея для лучшей модульности, тестируемости и упрощения разделения эффектов и чистой логики.

Когда использовать крючки

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

Точно так же, если ваш компонент:

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

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

import React, { useState } from 'react';
import t from 'prop-types';
import TextField, { Input } from '@material/react-text-field';
const noop = () => {};
const Holder = ({
  itemPrice = 175,
  name = '',
  email = '',
  id = '',
  removeHolder = noop,
  showRemoveButton = false,
}) => {
  const [nameInput, setName] = useState(name);
  const [emailInput, setEmail] = useState(email);
const setter = set => e => {
    const { target } = e;
    const { value } = target;
    set(value);
  };
return (
    <div className="row">
      <div className="holder">
        <div className="holder-name">
          <TextField label="Name">
            <Input value={nameInput} onChange={setter(setName)} required />
          </TextField>
        </div>
        <div className="holder-email">
          <TextField label="Email">
            <Input
              value={emailInput}
              onChange={setter(setEmail)}
              type="email"
              required
            />
          </TextField>
        </div>
        {showRemoveButton && (
          <button
            className="remove-holder"
            aria-label="Remove membership"
            onClick={e => {
              e.preventDefault();
              removeHolder(id);
            }}
          >
            &times;
          </button>
        )}
      </div>
      <div className="line-item-price">${itemPrice}</div>
      <style jsx>{cssHere}</style>
    </div>
  );
};
Holder.propTypes = {
  name: t.string,
  email: t.string,
  itemPrice: t.number,
  id: t.string,
  removeHolder: t.func,
  showRemoveButton: t.bool,
};
export default Holder;

Этот код использует useState для отслеживания состояний ввода эфемерной формы для имени и электронной почты:

const [nameInput, setName] = useState(name);
const [emailInput, setEmail] = useState(email);

Вы можете заметить, что в реквизитах Redux появился removeHolder создатель экшена. Смешивать и сочетать - это нормально.

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

Использование состояния компонента означало бы использование компонента class, установку начального состояния с помощью синтаксиса свойства экземпляра class (или функции constructor) и т. Д. - слишком много дополнительных сложностей, чтобы избежать Redux. Помогло то, что есть инструменты plug-and-play для управления состоянием формы с помощью Redux, поэтому мне не пришлось беспокоиться о том, что эфемерное состояние формы просочится в мою бизнес-логику.

Поскольку я уже использовал Redux во всех своих нетривиальных приложениях, выбор был прост: Redux (почти) все!

Теперь выбор по-прежнему прост:

Состояние компонента для состояния компонента, Redux для состояния приложения.

Когда использовать Redux

Другой распространенный вопрос: «стоит ли все класть в Redux?» Разве это не помешает отладке путешествий во времени, если вы этого не сделаете? »

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

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

Redux полезен, если ваш компонент:

  • Использует ввод-вывод, например API сети или устройства.
  • Сохраняет или загружает состояние.
  • Делит свое состояние с недочерними компонентами.
  • Имеет дело с любой бизнес-логикой или обработкой данных, совместно используемых другими частями приложения.

Вот еще один пример из приложения TDDDay:

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { compose } from 'ramda';
import page from '../../hocs/page.js';
import Purchase from './purchase-component.js';
import { addHolder, removeHolder, getHolders } from './purchase-reducer.js';
const PurchasePage = () => {
  // You can use these instead of
  // mapStateToProps and mapDispatchToProps
  const dispatch = useDispatch();
  const holders = useSelector(getHolders);
const props = {
    // Use function composition to compose action creators
    // with dispatch. See "Composing Software" for details.
    addHolder: compose(
      dispatch,
      addHolder
    ),
    removeHolder: compose(
      dispatch,
      removeHolder
    ),
    holders,
  };
return <Purchase {...props} />;
};
// `page` is a Higher Order Component composed of many
// other higher order components using function composition.
export default page(PurchasePage);

Этот компонент не обрабатывает DOM. Это компонент-контейнер, который делегирует задачи DOM импортированному компоненту презентации. Он подключается к Redux с помощью React-Redux hooks API.

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

Состояние, о котором он заботится, разделяется между компонентами, а не локализовано для одного компонента, оно сохраняется, а не эфемерно, и потенциально может охватывать несколько просмотров страниц или сеансов. Это все, с чем локальное состояние компонента не поможет, если вы не создадите свою собственную библиотеку контейнеров состояний поверх React API - а это намного сложнее, чем просто использование Redux.

В будущем Susse API React может помочь с сохранением и загрузкой состояния. Нам нужно подождать, пока API ожидания не появится, чтобы посмотреть, сможет ли он заменить мои шаблоны сохранения / загрузки в Redux. Redux позволяет нам четко отделить побочные эффекты от остальной логики компонента, не требуя имитации служб ввода-вывода. (Изоляция эффектов - вот почему я предпочитаю redux-saga thunks). Чтобы конкурировать с этим вариантом использования Redux, API React должен обеспечивать изоляцию эффектов.

Redux - это архитектура

Redux - это намного больше (а часто и меньше), чем библиотека управления состоянием. По сути, это также подмножество Архитектуры потока, которая более категорично относится к тому, как вносятся изменения состояния. У меня есть еще одно сообщение в блоге, в котором подробно рассказывается Архитектура Redux более подробно.

Я часто использую редукторы в стиле redux, когда мне нужно сложное состояние компонента, но мне не нужна библиотека Redux. Я также использую действия в стиле Redux (и даже инструменты Redux, такие как Autodux и redux-saga) для отправки действий в приложениях Node, без импорта библиотеки Redux.

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

Это отличная новость, если вы хотите начать использовать более локальное состояние компонента с помощью API хуков вместо того, чтобы использовать все функции Redux.

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

Если позже вам потребуется сохранить или поделиться состоянием, вы уже готовы на 90%. Все, что осталось, - это подключить компонент и добавить редуктор в хранилище Redux.

Больше вопросов и ответов

«Разве детерминизм сломается, если всего не в Redux?»

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

«Разве вам не нужен Redux как единый источник истины?»

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

Это означает, что вы можете выбрать, что входит в Redux, а что в состояние компонента. Вы также можете получить состояние из других источников, таких как API-интерфейсы браузера для текущего местоположения href.

Redux - отличный способ поддерживать единый источник истины для состояния вашего приложения, но если состояние вашего компонента локализовано для одного компонента и используется только в одном месте, по определению, у него уже есть единственный источник истины для этого состояния: Реагировать на состояние компонента.

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

Если нужно, можно поместить все в Redux. Могут быть последствия для производительности для состояния, которое необходимо часто обновлять, или для компонентов с большим количеством зависимых состояний. Вам не следует беспокоиться о производительности, если это не станет проблемой, но когда вы это сделаете, попробуйте оба способа и посмотрите, окажет ли это влияние. Profile и помните модель производительности RAIL.

«Что мне следует использовать: react-redux connect или API хуков?»

Это зависит от. connect создает повторно используемый компонент более высокого порядка, тогда как API хуков оптимизирован для интеграции с одним компонентом. Вам нужно будет подключить те же стойки к другим компонентам? Пойдите с connect. В противном случае я предпочитаю, как читает API хуков. Например, представьте, что у вас есть компонент, который обрабатывает авторизацию разрешений для действий пользователя:

import { connect } from 'react-redux';
import RequiresPermission from './requires-permission-component';
import { userHasPermission } from '../../features/user-profile/user-profile-reducer';
import curry from 'lodash/fp/curry';

const requiresPermission = curry(
  (NotPermittedComponent, { permission }, PermittedComponent) => {
    const mapStateToProps = state => ({
      NotPermittedComponent,
      PermittedComponent,
      isPermitted: userHasPermission(state, permission),
    });

    return connect(mapStateToProps)(RequiresPermission);
  },
);

export default requiresPermission;

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

import NextError from 'next/error';
import compose from 'lodash/fp/compose';
import React from 'react';
import requiresPermission from '../requires-permission';
import withFeatures from '../with-features';
import withAuth from '../with-auth';
import withEnv from '../with-env';
import withLoader from '../with-loader';
import withLayout from '../with-layout';

export default compose(
  withEnv,
  withAuth,
  withLoader,
  withLayout(),
  withFeatures,
  requiresPermission(() => <NextError statusCode={404} />, {
    permission: 'admin',
  }),
);

Чтобы использовать это:

import compose from 'lodash/fp/compose';
import adminPage from '../HOCs/admin-page';
import AdminIndex from '../features/admin-index/admin-index-component.js';

export default adminPage(AdminIndex);

API компонентов более высокого порядка удобен для этого варианта использования, и на самом деле он более лаконичен, чем API хуков (он требует меньше кода), но для того, чтобы читать connect API, вы должны помнить, что он принимает mapStateToProps в качестве первого аргумент и mapDispatchToProps в качестве второго аргумента, и вы должны знать, что он может принимать функции или объектные литералы, и вы должны знать различия в этом поведении. Также нужно помнить, что это карри, а не автоматическое карри.

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

«Если синглтоны - это анти-паттерн, а Redux - синглтон, то не является ли Redux анти-паттерном?»

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

Следующие шаги

Узнайте больше о React и Redux на EricElliottJS.com. Функциональные шаблоны, такие как композиция функций и частичное приложение, используемые в примерах кода в этой статье, подробно обсуждаются с множеством примеров и пошаговых видео.

Узнайте как модульное тестирование компонентов React, а, говоря о тестировании, узнайте о разработке через тестирование (TDD) на TDDDay.com.

Эрик Эллиотт - консультант по техническим продуктам и платформе, автор Composing Software, соучредитель EricElliottJS.com и DevAnywhere.io и наставник команды разработчиков. Он участвовал в разработке программного обеспечения для Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, и ведущие музыканты, в том числе Ашер, Фрэнк Оушен, Metallica и многие другие.

Он ведет уединенный образ жизни с самой красивой женщиной в мире.