С тех пор, как в React появились хуки, возникает много вопросов о том, способны ли они заменить Redux.
Я полагаю, что хуки и Redux имеют мало общего между собой. Хуки не дают нам неких новых удивительных возможностей по работе с состоянием. Они, вместо этого, расширяют API, позволяющие делать в React то, что в нём уже было возможным. Однако API хуков сделал гораздо удобнее работу со встроенными возможностями React по управлению состоянием. Оказалось, что новыми возможностями по работе с состоянием пользоваться легче, чем старыми, которые имелись в компонентах, основанных на классах. Теперь я гораздо чаще, чем раньше, использую средства по работе с состоянием компонентов. Естественно, поступаю я так лишь тогда, когда это уместно.
Для того чтобы объяснить моё отношение к хукам React и к Redux, мне хотелось бы сначала рассказать о том, в каких ситуациях обычно прибегают к использованию Redux.
Что такое Redux?
Redux — это библиотека, реализующая предсказуемое хранилище состояния приложений. Это, кроме того, архитектура, которая легко интегрируется с React.
Вот основные сильные стороны Redux:
- Детерминированное представление состояния (в комбинации с чистыми компонентами это даёт возможность формирования детерминированных визуальных элементов).
- Поддержка транзакционных изменений состояния.
- Изоляция управления состоянием от механизмов ввода-вывода данных и побочных эффектов.
- Наличие единого источника достоверных данных для состояния.
- Лёгкая организация совместной работы с состоянием в различных компонентах.
- Средства анализа транзакций (автоматическое логирование объектов действий).
- Отладка с возможностью записи и воспроизведения процесса выполнения программы (Time Travel Debugging, TTD).
Другими словами, Redux позволяет хорошо организовывать код и позволяет удобно его отлаживать. Redux помогает разрабатывать приложения, которые легко поддерживать. Благодаря использованию этой библиотеки упрощается поиск истоков проблем, возникающих в программах.
Что такое хуки React?
Хуки React позволяют, при работе с функциональными компонентами, пользоваться аналогом состояния компонентов, основанных на классах, и аналогами методов их жизненного цикла. Хуки появились в React 16.8.
Среди основных сильных сторон хуков можно отметить следующие:
- Возможность использования состояния и обработки событий жизненного цикла компонентов без применения компонентов, основанных на классах.
- Совместное хранение связанной логики в одном и том же месте компонента вместо разбиения подобной логики между несколькими методами жизненного цикла.
- Совместное использование механизмов, независимых от реализации компонента (это похоже на шаблон render prop).
Обратите внимание на то, что эти замечательные возможности, на самом деле, не перекрывают возможности Redux. Хуки React можно и нужно использовать для того, чтобы выполнять детерминированные обновления состояния, но это всегда было одной из возможностей React, и детерминированная модель состояния Redux хорошо совмещается с этой возможностью. Именно так React добивается детерминированности в выводе визуальных элементов, и это, без преувеличений, один из движущих мотивов создания React.
Если пользоваться инструментами наподобие API react-redux с поддержкой хуков, или хуком React useReducer, окажется, что нет причины задавать вопрос о том, что выбрать — хуки или Redux. Можно пользоваться и тем и другим, сочетая и комбинируя эти технологии.
Что заменяют хуки?
После появления API хуков я перестал использовать следующие технологии:
- Компоненты, основанные на классах.
- Паттерн render-prop.
Что не заменяют хуки?
Я всё ещё часто пользуюсь следующими технологиями:
- Redux — по всем вышеперечисленным причинам.
- Компонентами высшего порядка — для целей выполнения композиции компонентов в тех случаях, когда мне приходится реализовывать сквозной функционал, совместно используемый всеми или некоторыми визуальными компонентами приложения. К подобному функционалу относятся провайдеры Redux, системы построения макетов страниц, системы поддержки настроек приложения, средства аутентификации и авторизации, средства интернационализации приложений и так далее.
- Разделением между компонентами-контейнерами и компонентами, имеющими визуальное представление. Это позволяет улучшить модульность и тестируемость приложений, лучше разделить эффекты и чистую логику.
Когда стоит пользоваться хуками?
Нет нужды стремиться к тому, чтобы использовать Redux в каждом приложении и в каждом компоненте. Если ваш проект состоит из одного визуального компонента, если он не сохраняет данные в состояние и не загружает их оттуда, если в нём не выполняются асинхронные операции ввода-вывода, то я не могу придумать достойной причины усложнять этот проект за счёт использования в нём Redux.
То же самое можно сказать и о компонентах, обладающих следующими особенностями:
- Они не пользуются сетевыми ресурсами.
- Они не сохраняют данные в состояние и не загружают их оттуда.
- Они не пользуются состоянием совместно с другими компонентами, не являющимися их потомками.
- У них нет некоего собственного состояния, используемого для кратковременного хранения данных.
У вас могут быть веские основания для использования в определённых ситуациях стандартной модели состояния компонентов React. В подобных ситуациях хорошую службу вам сослужат хуки React. Например, форма, код которой приведён ниже, использует локальное состояние компонента с помощью хука React useState
.
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);
}}
>
×
</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
используется для управления кратковременно используемым состоянием полей ввода name
и email
:
const [nameInput, setName] = useState(name);
const [emailInput, setEmail] = useState(email);
Вы можете заметить, что здесь ещё имеется создатель действия removeHolder
, поступающий в свойства из Redux. Как уже было сказано, сочетание и комбинирование технологий — это совершенно нормально.
Использование локального состояния компонента для решения подобных задач всегда выглядело хорошо, но до появления хуков React у меня бы, в любом случае, возникло бы желание сохранять данные компонента в Redux-хранилище и получать состояние из свойств.
Раньше работа с состоянием компонента предусматривала использование компонентов, основанных на классах, запись начальных данных в состояние с применением механизмов объявления свойств класса (или в конструкторе класса) и так далее. В результате оказывалось, что для того, чтобы избежать использования Redux, компонент приходилось слишком сильно усложнять. В пользу Redux говорило и существование удобных инструментов для управления состоянием форм с помощью Redux. В результате раньше я не беспокоился бы о том, что временное состояние формы хранится там же, где и данные, имеющие более длительный срок существования.
Так как я уже использовал Redux во всех моих мало-мальски сложных приложениях, выбор технологии для хранения состояния компонентов особых раздумий у меня не вызывал. Я просто пользовался Redux практически во всех случаях.
В современных условиях делать выбор тоже несложно: работа с состоянием компонента организуется с помощью стандартных механизмов React, а управление состоянием приложения — с помощью Redux.
Когда стоит пользоваться Redux?
Ещё один распространённый вопрос, касающийся управления состоянием, звучит так: «Нужно ли мне помещать абсолютно всё в хранилище Redux? Если я этого не сделаю, не нарушит ли это возможности по отладке приложений с использованием механизмов TTD?».
Нет нужды размещать абсолютно всё в хранилище Redux. Дело в том, что в приложениях используется много временных данных, которые слишком сильно по нему разбросаны для того, чтобы дать какую-то информацию, которая, будучи записанной в журнал или применённой при отладке, может оказать разработчику заметную помощь в поиске проблем. Вероятно, вам, если только вы не пишете приложение-редактор, работающее в реальном времени, нет нужды записывать в состояние каждое движение мыши или каждое нажатие на клавишу клавиатуры. Когда вы помещаете нечто в состояние Redux, вы добавляете в приложение дополнительный уровень абстракции, а также — сопутствующий ему дополнительный уровень сложности.
Другими словами, вы можете спокойно пользоваться Redux, но на это у вас должны быть некие причины. Применение возможностей Redux в компонентах может оказаться оправданным в том случае, если компоненты отличаются следующими особенностями:
- Они пользуются средствами ввода-вывода. Например — работают с сетью или с некими устройствами.
- Они сохраняют данные в состояние или загружают их из него.
- Они работают со своим состоянием совместно с компонентами, не являющимися их потомками.
- Они имеют дело с любой бизнес-логикой, с которой имеют дело и другие части приложения, либо — выполняют обработку данных, которые используются в других частях приложения.
Вот ещё один пример, взятый из приложения 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 = () => {
// Это можно использовать вместо
// mapStateToProps и mapDispatchToProps
const dispatch = useDispatch();
const holders = useSelector(getHolders);
const props = {
// Воспользуемся композицией функций для конструирования создателей
// действий с помощью dispatch.
addHolder: compose(
dispatch,
addHolder
),
removeHolder: compose(
dispatch,
removeHolder
),
holders,
};
return <Purchase {...props} />;
};
// `page` - это компонент высшего порядка, созданный из множества
// других компонентов высшего порядка с помощью композиции функций.
export default page(PurchasePage);
Этот документ не занимается работой с DOM. Это — презентационный компонент. Он подключён к Redux с использованием API react-redux с поддержкой хуков.
Redux здесь используется из-за того, что нам нужно, чтобы данные, которыми занимается эта форма, можно было бы использовать и в других частях пользовательского интерфейса. А после того, как операция покупки будет завершена, нам надо сохранить соответствующую информацию в базе данных.
Фрагменты состояния, с которыми работает этот код, используются различными компонентами, они не обрабатываются лишь одним компонентом. Это — не данные, существующие лишь небольшой отрезок времени. Эти данные можно считать постоянными, они вполне могут использоваться на разных экранах приложения и в нескольких сессиях. Всё это — сценарии, в которых состояния компонента для хранения данных применить не удастся. Это, правда, всё же возможно, но только в том случае, если создатель приложения напишет, на базе API React, собственную библиотеку для управления состоянием. Подобное сделать гораздо сложнее, чем просто воспользоваться Redux.
API React Suspense, в будущем, может пригодиться при выполнении сохранения данных в состоянии и загрузки их из него. Нам нужно подождать его выхода и посмотреть — сможет ли оно заменить шаблоны сохранения и загрузки данных Redux. Redux позволяет нам чётко отделять побочные эффекты от остальной логики компонента, при этом нам не нужно особым образом работать со службами ввода-вывода. (Причиной того, что я предпочитаю библиотеку redux-saga промежуточному ПО redux-thunk, является изоляция эффектов). Для того чтобы соревноваться в этом сценарии с Redux, API React понадобится обеспечить изоляцию эффектов.
Redux — это архитектура
Redux — это гораздо больше (а часто — и гораздо меньше), чем библиотека для управления состоянием. Это ещё и подмножество архитектуры Flux, которая гораздо более жёстко определяет то, как выполняются изменения состояния. Подробности об архитектуре Redux можно почитать здесь.
Я часто использую редьюсеры, созданные в стиле Redux, в тех случаях, когда мне нужно поддерживать сложное состояние компонента, но не нужно пользоваться библиотекой Redux. Я, кроме того, пользуюсь действиями, созданными в духе Redux (и даже Redux-инструментами вроде Autodux и redux-saga) для отправки действий в Node.js-приложениях. При этом я даже не импортирую в подобные приложения Redux.
Проект Redux всегда был больше архитектурой и набором добровольно соблюдаемых соглашений, чем библиотекой. На самом деле, базовую реализацию Redux можно уложить буквально в пару десятков строк кода.
Это окажется хорошей новостью для тех, кому хочется чаще использовать локальные состояния компонентов с помощью хуков и при этом не привязывать всё к Redux.
React поддерживает хук useReducer
, который может работать с редьюсерами, написанными в стиле Redux. Это хорошо для реализации нетривиальной логики работы с состоянием, для работы с зависимыми фрагментами состояния, и так далее. Если вы встретились с задачей, для решения которой подойдёт временное состояние отдельного компонента, то для работы с этим состоянием вы можете воспользоваться архитектурой Redux, но вместо библиотеки Redux для управления состоянием можете применить хук useReducer
.
Если позже вам понадобится наладить постоянное хранение данных, которые раньше вы хранили лишь временно, то вы будете на 90% готовы к подобному изменению. Всё, что вам останется сделать — это подключить компонент к хранилищу Redux и добавить туда соответствующий редьюсер.
Вопросы и ответы
▍Нарушается ли детерминизм в том случае, если Redux управляет не всеми данными приложения?
Нет, не нарушается. На самом деле, использование Redux не делает проект детерминированным. А вот соглашения — делают. Если вы хотите, чтобы ваше Redux-состояние было бы детерминированным — используйте чистые функции. То же самое касается и ситуаций, в которых нужно, чтобы детерминированным бы было временное состояние локальных компонентов.
▍Должна ли библиотека Redux играть роль единого источника достоверных данных?
Принцип единого источника достоверных данных не указывает на то, что нужно, чтобы все данные, входящие в состояние приложения, хранились бы в одном месте. Смысл этого принципа заключается в том, что у каждого фрагмента состояния должен быть лишь один источник достоверных данных. В результате у нас может быть множество фрагментов состояния, у каждого из которых есть собственный источник достоверных данных.
Это означает, что программист может принять решение о том, что передаётся в Redux, и о том, что передаётся в состояние компонентов. Данные, определяющие состояние, можно брать и из других источников. Например — из браузерного API, позволяющего работать со сведениями об адресе просматриваемой страницы.
Redux — это отличный инструмент для поддержки единого источника достоверных данных для состояния приложения. Но если состояние компонента находится и используется исключительно в пределах этого компонента, то, по определению, у этого состояния уже имеется единственный источник достоверных данных — состояние компонента React.
Если вы помещаете некие данные в состояние Redux, вы всегда должны выполнять чтение этих данных из состояния Redux. Для всего, что находится в хранилище Redux, это хранилище должно выступать единственным источником достоверных данных.
Если нужно, то помещать всё в состояние Redux — это совершенно нормально. Возможно, это повлияет на производительность в случае использования фрагментов состояния, которые нужно часто обновлять, или в том случае, если речь идёт о хранении состояния компонента, в котором интенсивно используются зависимые фрагменты состояния. О производительности не стоит беспокоиться до тех пор, пока с производительностью не возникнут проблемы. Но если вас вопрос производительности беспокоит — попробуйте оба способа работы с состоянием и оцените их воздействие на производительность. Выполните профилирование проекта и помните о модели производительности RAIL.
▍Нужно ли мне использовать функцию connect из react-redux, или лучше использовать хуки?
Это зависит от многого. Функция connect
создаёт компонент высшего порядка, подходящий для многократного использования, а хуки оптимизированы для интеграции с отдельным компонентом.
Нужно ли подключать одни и те же свойства к разным компонентам? Если это так — пользуйтесь connect
. В противном случае я предпочёл бы выбрать хуки. Например, представьте, что у вас имеется компонент, который отвечает за авторизацию разрешений для действий пользователя:
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 компонентов высшего порядка оказывается удобным для решения этой задачи. Оно позволяет решить её более лаконично, с использованием меньшего объёма кода, чем применение хуков. А вот для того чтобы задействовать функцию connect
, нужно помнить о том, что она, в качестве первого аргумента, принимает mapStateToProps
, а в качестве второго — mapDispatchToProps
. Нужно не забывать о том, что эта функция может принимать функции или объектные литералы. Нужно знать о том, чем отличаются разные варианты использования connect
, и о том, что это — каррированная функция, но её каррирование не выполняется автоматически.
Другими словами можно сказать, что я полагаю, что при разработке connect
была проведена большая работа в направлении лаконичности кода, но получающийся код не оказывается ни особенно хорошо читаемым, ни особенно удобным. Если мне не нужно работать с несколькими компонентами, то я с удовольствием предпочту неудобному API connect
гораздо более удобный API хуков, даже учитывая то, что это приведёт к росту объёма кода.
▍Если синглтон считается анти-паттерном, а Redux является синглтоном, значит ли это, что Redux — это анти-паттерн?
Нет, не значит. Использование в коде паттерна синглтон намекает на сомнительное качество этого кода, указывая на наличие в нём совместно используемого изменяемого состояния. Вот это — настоящий анти-паттерн. Redux же предотвращает мутацию разделяемого состояния через инкапсуляцию (не следует менять состояние приложения напрямую, за пределами редьюсеров; задачи по изменению состояния решает Redux) и через передачу сообщений (изменение состояния может вызвать лишь отправленный объект события).
Итоги
Заменяют ли Redux хуки React? Хуки — это замечательно, но Redux они не заменяют.
Надеемся, что этот материал поможет вам в деле выбора модели управления состоянием для ваших React-проектов.
Уважаемые читатели! Встречались ли вы с ситуациями, в которых хуки React способны заменить Redux?
Автор: ru_vds