Реакт это все что вам нужно для управления состоянием вашего приложения.
Управление состоянием это одна из сложнейших задач при разработки приложения. Вот почему каждый день появляются все новые и новые библиотеки для управления состоянием, их становится все больше и больше, причем многие из них разрабатываются поверх уже существующих решений. В npm вы можете найти сотни "упрощенных Redux" библиотек. Однако, несмотря на то что управлять состоянием сложно, одной из причин того почему так получилось стало именно то что мы слишком переусложняем решение проблемы.
Существует метод управления состоянием который лично я пытаюсь применять еще с тех пор как я начал использовать Реакт. И теперь, после релиза хуков (hooks) и улучшения контекстов (context), этот метод управления состояниями стало очень просто использовать.
О компонентах Реакта часто говорят как о детальках Лего конструктора, из которых мы собираем наши приложения. Но эту аналогию можно применить не только к компонентам, но и к состоянию приложения. "Секрет" моего подхода к управлению состоянием в том что состояние приложения должно соответствовать структуре самого приложения.
Причиной популярности Редакса (redux), помимо прочего, стало то что react-redux решал проблему проп дриллинга (prop drilling). Редакс позволил обмениваться данными между различными частями дерева компонентов, просто передавая компонент в магическую функцию connect
. Другие возможности Редакса — редюсеры, экшены и прочее, конечно хороши, но я уверен что повсеместное использование Редакса связано именно с тем, что он позволил избавиться от проп дриллинга.
Проп дриллинг это такой анти-паттерн при котором передача пропсов происходит через промежуточные компоненты которые не используют получаемые пропсы, а только передают их в следующие компоненты.
Но применение Редакса может привести к различным проблемам. Я часто вижу как разработчики переносят все состояния приложения в Редакс. Не только глобальное состояние, но и локальные. Это приводит к тому что когда вы создаете любое взаимодействие с состоянием, оно запускает взаимодействие с редюсерами, генераторами/типами экшенов и вызовами dispatch (dispatch calls). Из-за этого, просто чтобы понять как и какие стейты оказывают влияние на приложение, вам нужно открывать кучу файлов и отслеживать весь написанный там код.
Не поймите меня неправильно, это хороший подход для состояний которые действительно должны быть глобальными, но для простых состояний (таких как, например, открыто ли модальное окно или состояние строки ввода у формы) это большая проблема. Что делает ситуацию еще хуже, так это то что такой подход плохо масштабируется. Чем больше становится ваше приложение, тем хуже становится эта проблема. Конечно, вы можете подключать различные редюсеры для управления теми или иными частями приложения, но косвенная обработка всех этих генераторов экшенов и редюсеров не является оптимальной.
Держать все состояние вашего приложения в одном объекте не лучшая идея и может привести к другим проблемам (в том числе если вы не используете для этого Редакс). Когда Реакт <Context.Provider>
получает новое значение, все компоненты, которые используют это значение, обновляются и запускают рендер, даже если это функциональный компонент который отвечает только за какую-то часть данных. Это может привести к проблемам с производительностью. Что я хочу сказать — у вас не будет подобных проблем если ваше состояние разделено и находится в дереве компонентов Реакта таким образом чтобы быть как можно ближе к тем местам к которым это состояние и относиться.
Тут вот какое дело — если вы создаете приложение при помощи React, у вас уже установлен пакет для управления состоянием. Чтобы использовать его, вам не нужно применять npm install
или yarn add
. Этот пакет не добавляет лишних байтов в ваше приложение, он уже интегрирован со всеми библиотеками для Реакта, и он уже хорошо документирован командой Реакта. Это сам Реакт.
Реакт это библиотека для управления состоянием
Когда вы создаете приложение при помощи Реакта, вы собираете множество компонентов, чтобы создать дерево компонентов. Вы начинаете с вашего <App />
и заканчиваете низкоуровневыми <input />
, <div />
и <button />
. Вы не управляете всеми низкоуровневыми составными компонентами, которые ваше приложение рендерит, из какого-то централизованного места. Вместо этого вы позволяете каждому отдельному компоненту управлять им. Как оказалось, это действительно простой и эффективный способ создания UI.
Такой же подход можно применить и к состоянию:
function Counter() {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return <button onClick={increment}>{count}</button>
}
function App() {
return <Counter />
}
Имейте в виду что все, о чем здесь идет речь, работает и с классами. Хуки просто упрощают работу (особенно работу с контекстом, вскоре мы рассмотрим и такой вариант).
class Counter extends React.Component {
state = {count: 0}
increment = () => this.setState(({count}) => ({count: count + 1}))
render() {
return <button onClick={this.increment}>{this.state.count}</button>
}
}
"Окей, это конечно легко — управлять одним элементом состояния в одном компоненте, но что если мне нужно разделить это состояние между компонентами? Например, что, если я хочу сделать это:
function CountDisplay() {
// откуда нам брать значение для `count`?
return <div>The current counter count is {count}</div>
}
function App() {
return (
<div>
<CountDisplay />
<Counter />
</div>
)
}
"Управление состоянием для подсчета значения происходит внутри <Counter />
, выходит, теперь мне нужна библиотека управления состоянием, чтобы получить доступ к значению count
для <CountDisplay />
и для его обновлений в <Counter />
!"
Ответ этот вопрос настолько же стар, настолько стар и сам Реакт (или старше?), и был в документации столько, сколько я себя помню: Подъём состояния
Подъём состояния (Lifting State Up) это надежный и рекомендуемый способ управления состоянием в Реакте. Вот каким образом можно применять его:
function Counter({count, onIncrementClick}) {
return <button onClick={onIncrementClick}>{count}</button>
}
function CountDisplay({count}) {
return <div>The current counter count is {count}</div>
}
function App() {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return (
<div>
<CountDisplay count={count} />
<Counter count={count} onIncrementClick={increment} />
</div>
)
}
Теперь за управление состоянием отвечает другой компонент. При необходимости вы всегда можете поднять управление состоянием в вышестоящий компонент, вплоть до самого верхнего компонента.
"Да, конечно, но что на счет проблемы проп дриллинга (prop drilling)?"
На самом деле это проблема у которой уже довольно давно есть решение — контексты (context
). На протяжении долгого времени люди применяли react-redux
из-за предупреждений в документации Реакта об использовании контекстов. Но сейчас контексты это официально поддерживаемая часть React API, и мы можем использовать их напрямую:
// src/count/count-context.js
import React from 'react'
const CountContext = React.createContext()
function useCount() {
const context = React.useContext(CountContext)
if (!context) {
throw new Error(`useCount must be used within a CountProvider`)
}
return context
}
function CountProvider(props) {
const [count, setCount] = React.useState(0)
const value = React.useMemo(() => [count, setCount], [count])
return <CountContext.Provider value={value} {...props} />
}
export {CountProvider, useCount}
// src/count/page.js
import React from 'react'
import {CountProvider, useCount} from './count-context'
function Counter() {
const [count, setCount] = useCount()
const increment = () => setCount(c => c + 1)
return <button onClick={increment}>{count}</button>
}
function CountDisplay() {
const [count] = useCount()
return <div>The current counter count is {count}</div>
}
function CountPage() {
return (
<div>
<CountProvider>
<CountDisplay />
<Counter />
</CountProvider>
</div>
)
}
ПРИМЕЧАНИЕ. Этот код является просто примером, и я НЕ рекомендую использовать контекст для решения конкретно этой проблемы. В данном случае более простым решением стало бы просто передача состояний через пропсы (подробнее здесь: Prop Drilling). Не нужно применять контексты там где можно обойтись более простыми методами.
Одна из крутейших особенностей этого решения заключается в том что мы можем абстрагировать всю логику которую часто применяем для обновления состояния в наш useContext
хук:
function useCount() {
const context = React.useContext(CountContext)
if (!context) {
throw new Error(`useCount must be used within a CountProvider`)
}
const [count, setCount] = context
const increment = () => setCount(c => c + 1)
return {
count,
setCount,
increment,
}
}
При желании можно поменять useState
на useReducer
:
function countReducer(state, action) {
switch (action.type) {
case 'INCREMENT': {
return {count: state.count + 1}
}
default: {
throw new Error(`Unsupported action type: ${action.type}`)
}
}
}
function CountProvider(props) {
const [state, dispatch] = React.useReducer(countReducer, {count: 0})
const value = React.useMemo(() => [state, dispatch], [state])
return <CountContext.Provider value={value} {...props} />
}
function useCount() {
const context = React.useContext(CountContext)
if (!context) {
throw new Error(`useCount must be used within a CountProvider`)
}
const [state, dispatch] = context
const increment = () => dispatch({type: 'INCREMENT'})
return {
state,
dispatch,
increment,
}
}
Это дает нам гибкость и уменьшает сложность кода. Вот о чем следует помнить когда вы так делаете:
Не нужно держать все в одном объекте состояния. Разделяйте логику состояния, используйте разные контексты для разных ситуаций, например, пользовательские настройки не обязательно должны находиться в контексте в котором уже находятся уведомления.
Не нужно делать все контексты глобальными! Держите состояние как можно ближе к месту к которому оно относится.
Подробнее о втором пункте. Структура вашего приложения может выглядеть примерно так:
function App() {
return (
<ThemeProvider>
<AuthenticationProvider>
<Router>
<Home path="/" />
<About path="/about" />
<UserPage path="/:userId" />
<UserSettings path="/settings" />
<Notifications path="/notifications" />
</Router>
</AuthenticationProvider>
</ThemeProvider>
)
}
function Notifications() {
return (
<NotificationsProvider>
<NotificationsTab />
<NotificationsTypeList />
<NotificationsList />
</NotificationsProvider>
)
}
function UserPage({username}) {
return (
<UserProvider username={username}>
<UserInfo />
<UserNav />
<UserActivity />
</UserProvider>
)
}
function UserSettings() {
// это специальный кастомный хук для AuthenticationProvider
const {user} = useAuthenticatedUser()
}
Обратите внимание что у каждой страницы может быть свой собственный провайдер (provider) контекста, который передает данные необходимые компоненту находящемуся под ним. При таком подходе разделение кода (Code-Splitting) работает само по себе. То, как вы передаете данные в каждый провайдер, зависит от того как эти провайдеры используют хуки, и от того каким образом вы извлекаете данные в своем приложении. В любом случае чтобы понять как работает ваш контекст, в первую очередь вам нужно посмотреть в код компонента-провайдера.
Если хотите узнать больше о том что такое "совместное размещение", читайте статью State Colocation will make your React app faster (на русском как сделать React приложение быстрее при помощи совместного размещения состояний
) и Colocation. А если интересно почитать больше о работе с контекстами, читайте статью How to use React Context effectively
Кэш Сервера против Состояние Интерфейса
Существуют различные виды состояний приложения, но каждый тип состояния можно отнести к одной из этих категорий:
Кэш Сервера (Server Cache) — состояние которое размещено на сервере для быстрого доступа к нему на клиенте (к примеру — данные пользователя).
Состояние Интерфейса (UI State) — состояние в котором есть смысл только в интерфейсе пользователя, оно нужно для управления интерактивными частями приложения (к примеру, открытие модального окна — modal
isOpen
)
Мы совершаем ошибку когда относимся к этим двум одинаково. Кэш сервера сильно отличается от состояния UI, и к нему нужен иной подход. Если вы поняли что ваше состояние это вовсе не состояние, а кэш состояния, то вы начинаете лучше понимать ваше состояние и то как нужно им управлять.
Вы определенно можете управлять им при помощи ваших собственных useState
или useReducer
, с правильными useContext
там и тут. Но, имейте в виду, кэширование это очень сложная проблема (некоторые говорят что это одна из сложнейших проблем в информатике), так что, касательно этого вопроса, будет разумно "встать на плечи гигантов".
Я сам использую, и всячески рекомендую библиотеку react-query для подобных состояний. Знаю, знаю, я сам сказал что вам не нужны библиотеки для управления состоянием, но, я не считаю что react-query это библиотека для управления состоянием. Я считаю что это библиотека для управления кэшем. И она офигенно хороша. Попробуйте ее. Этот парень — Tanner Linsley весьма умен.
Заключение
Как я уже говорил, все это вы можете реализовать применяя классовые компоненты (вам не обязательно использовать хуки). Хуки просто делают все намного проще, но вы можете реализовать эту философию и в React 15. Опускайте состояние как можно ниже по иерархии компонентов, и используйте контекст только тогда когда проп дриллинг реально станет проблемой. Все эти действия помогут вам упростить работу с состоянием вашего приложения.
Fen1kz
Недавно работал над проектом где для данных использовался Apollo graphQL и, соответственно, не было редакса за ненадобностью.
Такое себе удовольствие, если честно. Особенно когда у тебя есть роут, на котором, допустим, определяются какие-то специфичные для этого роута вещи, а потом вдруг прилетает требование отобразить их в сайдбаре. Вот начинается веселье.
Ах да, и модалки которые в редаксе делаются императивно
dispatch(showModal(modalName))
было очень больно делать декларативно. Есть вроде чистый компонент таблицы какой-нибудь и давай его пачкать стейтом isVisible итд.А если все делать через кучу провайдеров — так зачем заменять единый стор и SSOT на кучу разных "сервисов". А если они ещё друг от друга будут зависеть, уххх.
Типа как в примере автора — прилетит "хотим при логине показывать нотификацию". Оу, сорян, у нас нотификейшн провайдер ниже аутентификационного.
IvanGanev Автор
Чем больше провайдеров, тем, следовательно, и альтернативный вариант с единым стором будет сложнее. Единый стор не будет проще просто из-за того что это единый стор.
Поднять провайдер не проблема. Хотя в этом случае можно сделать новый провайдер (специально для этого нотификейшена) или добавить это в логику самого аутентификационного провайдера. Почему бы и нет? Сами по себе провайдеры могут иметь внутри сложную логику.
Fen1kz
Единый стор будет проще за счет того, что а) не надо будет синхронизировать провайдеры.
б) не надо будет дублировать функционал
Кажется, что тут из ошибок только ться, но на самом деле это совет смешивать логику и представление. Если вам понадобится изменить представление, то и логику придется менять. Не надо так.
Про модалы — в статье написано, что отображением модала должна заниматься та страница, которая его отображает. Это кажется хорошей идеей, но ровно до тех пор, пока ваш крутой, реюзабельный модал не понадобится на второй странице и вам придется дублировать код его открытия, закрытия, закрытия если запрос отправился итд.
Никому такого не желаю
Про редакс — основная фишка редакса это не избавление от prop-drilling'а. Это разделение мутаций и асинхронности. Я знаю что есть useReducer, но просто чтобы не смущали народ фразами типа "повсеместное использование Редакса связано именно с тем, что он позволил избавиться от проп дриллинга."
Modin
И потом в коде можно будет вызывать как