Как управлять состоянием React приложения без сторонних библиотек

Автор оригинала: Kent C. Dodds
  • Перевод
  • Tutorial

image


Реакт это все что вам нужно для управления состоянием вашего приложения.


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

Edit React Codesandbox


Имейте в виду что все, о чем здесь идет речь, работает и с классами. Хуки просто упрощают работу (особенно работу с контекстом, вскоре мы рассмотрим и такой вариант).


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

Edit React Codesandbox


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


"Да, конечно, но что на счет проблемы проп дриллинга (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>
  )
}

Edit React Codesandbox


ПРИМЕЧАНИЕ. Этот код является просто примером, и я НЕ рекомендую использовать контекст для решения конкретно этой проблемы. В данном случае более простым решением стало бы просто передача состояний через пропсы (подробнее здесь: 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,
  }
}

Edit React Codesandbox


При желании можно поменять 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,
  }
}

Edit React Codesandbox


Это дает нам гибкость и уменьшает сложность кода. Вот о чем следует помнить когда вы так делаете:


  1. Не нужно держать все в одном объекте состояния. Разделяйте логику состояния, используйте разные контексты для разных ситуаций, например, пользовательские настройки не обязательно должны находиться в контексте в котором уже находятся уведомления.


  2. Не нужно делать все контексты глобальными! Держите состояние как можно ближе к месту к которому оно относится.



Подробнее о втором пункте. Структура вашего приложения может выглядеть примерно так:


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


Кэш Сервера против Состояние Интерфейса


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


  1. Кэш Сервера (Server Cache) — состояние которое размещено на сервере для быстрого доступа к нему на клиенте (к примеру — данные пользователя).


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



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


Вы определенно можете управлять им при помощи ваших собственных useState или useReducer, с правильными useContext там и тут. Но, имейте в виду, кэширование это очень сложная проблема (некоторые говорят что это одна из сложнейших проблем в информатике), так что, касательно этого вопроса, будет разумно "встать на плечи гигантов".


Я сам использую, и всячески рекомендую библиотеку react-query для подобных состояний. Знаю, знаю, я сам сказал что вам не нужны библиотеки для управления состоянием, но, я не считаю что react-query это библиотека для управления состоянием. Я считаю что это библиотека для управления кэшем. И она офигенно хороша. Попробуйте ее. Этот парень — Tanner Linsley весьма умен.


Заключение


Как я уже говорил, все это вы можете реализовать применяя классовые компоненты (вам не обязательно использовать хуки). Хуки просто делают все намного проще, но вы можете реализовать эту философию и в React 15. Опускайте состояние как можно ниже по иерархии компонентов, и используйте контекст только тогда когда проп дриллинг реально станет проблемой. Все эти действия помогут вам упростить работу с состоянием вашего приложения.

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 21

    +4

    Недавно работал над проектом где для данных использовался Apollo graphQL и, соответственно, не было редакса за ненадобностью.


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


    Ах да, и модалки которые в редаксе делаются императивно dispatch(showModal(modalName)) было очень больно делать декларативно. Есть вроде чистый компонент таблицы какой-нибудь и давай его пачкать стейтом isVisible итд.


    А если все делать через кучу провайдеров — так зачем заменять единый стор и SSOT на кучу разных "сервисов". А если они ещё друг от друга будут зависеть, уххх.


    Типа как в примере автора — прилетит "хотим при логине показывать нотификацию". Оу, сорян, у нас нотификейшн провайдер ниже аутентификационного.

      0
      А если все делать через кучу провайдеров — так зачем заменять единый стор и SSOT на кучу разных «сервисов». А если они ещё друг от друга будут зависеть, уххх.


      Чем больше провайдеров, тем, следовательно, и альтернативный вариант с единым стором будет сложнее. Единый стор не будет проще просто из-за того что это единый стор.

      Типа как в примере автора — прилетит «хотим при логине показывать нотификацию». Оу, сорян, у нас нотификейшн провайдер ниже аутентификационного.


      Поднять провайдер не проблема. Хотя в этом случае можно сделать новый провайдер (специально для этого нотификейшена) или добавить это в логику самого аутентификационного провайдера. Почему бы и нет? Сами по себе провайдеры могут иметь внутри сложную логику.

        0
        Единый стор не будет проще просто из-за того что это единый стор.

        Единый стор будет проще за счет того, что а) не надо будет синхронизировать провайдеры.


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

        б) не надо будет дублировать функционал




        Не нужно делать все контексты глобальными! Держите состояние как можно ближе к месту к которому оно относиться.

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




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


        Никому такого не желаю




        Про редакс — основная фишка редакса это не избавление от prop-drilling'а. Это разделение мутаций и асинхронности. Я знаю что есть useReducer, но просто чтобы не смущали народ фразами типа "повсеместное использование Редакса связано именно с тем, что он позволил избавиться от проп дриллинга."

        +1
        Ах да, и модалки которые в редаксе делаются императивно dispatch(showModal(modalName)) было очень больно делать декларативно. Есть вроде чистый компонент таблицы какой-нибудь и давай его пачкать стейтом isVisible итд.


        import React from 'react';
        import ReactDOM from 'react-dom';
        
        const confirmRoot = document.createElement('div');
        const body = document.querySelector('body');
        body.appendChild(confirmRoot);
        
        const customConfirm = DialogContent =>
          new Promise(res => {
            const giveAnswer = answer => {
              ReactDOM.unmountComponentAtNode(confirmRoot);
              res(answer);
            };
            ReactDOM.render(<DialogContent giveAnswer={giveAnswer} />, confirmRoot);
          });
        
        export default customConfirm;


        И потом в коде можно будет вызывать как

        customConfirm(Component)/await customConfirm(Component)


        –3
        Реакт это все что вам нужно для управления состоянием вашего приложения.

        Для уровня Hello World и сверх простого Todo List да. А по правде так:
        Стейт менеджмент реакта ровно такое же говно, как и redux и ему подобный не реактивный, иммутабильный, примитивный и делитанский булшит, который принципиально не раскрывает богатые возможности JS и заставляет писать лютый и не поддерживаемый говнокод под видом «ну это best practies же». Посмотрите по сторонам, Vue реактивный, Svelte реактивный, но React же не реактивный по сей день и это просто смешно и абсурдно, благо благодаря MobX'у он стал реактивным с 2016 года.
          +4
          «Держите состояние как можно ближе к месту к которому оно относиться» — я придерживаюсь другой парадигмы, в разработке приложений, в которых участвовал, локальные стейты приводили к дубляжу и сложному контролю. Идея о том, что можно хранить информацию о состоянии приложения в едином стейте, и каждый компонент может в любое время получить информацию о любой части и взять под контроль ее отображение, стала значительным апгрейдом в плане проектирования удобных систем.

          Нужно ли закрыть модалку по сабмиту формы, добавить валидации к полям, исходя из значений в форме перерисовать лэйаут, в едином месте настраивать аналитику (вместо интеграции в каждый компонент), получить более низкоуровневый доступ к верстке компонента, сделать слепок состояния и восстановить для различных целей — глобальное состояние удобнее в огромном количестве реальных задач. А локальное, хотя кажется и не очень удобным выносить tooltipIsShown наружу из компонента Tooltip, но в реальности это непередаваемо удобней, когда можно закрывать все тултипы или показывать нужный по независимой от ховера мышки логике, вместо `querySelectorAll('[data-role=«tooltip»]').click()` — и тут еще нужно проверить состояние, чтобы не закрылись/открылись лишние. А такого кода параллельно с Реактом видел неприлично много.

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

          По поводу контекстов — думаю, Кент глаза никому не открыл, при изучении Реакта все так или иначе знакомятся с этой основополагающей концепцией) А вот мысль разбивать хранилище на разноуровневые и подключать каждое отдельно — глупость. Что мешает сделать один StateProvider, и внутри компонентов выбирать нужную его часть?

          // Don't
          const { user } = useAuthenticatedUser()
          const { notifications } = useNotifications()
          
          // Do
          const { user, notifications } = useStore()
          


          Импортов и кода меньше, возиться с уровнями и отсутствием проброшенного не нужно. А внутри стора можно как угодно скомпоновать логику и методы, удобно разбив по нескольким файлам.

          Только вот от стора требуется реактивность, а от коннектора к компоненту — имплементация shouldComponentUpdate, чтобы компонент обновлялся только при изменении нужных ему данных. В случае с чистыми контекстами ни реакций на изменение, ни оптимизированных ререндеров ждать не приходится, да и обкладываться вот таким

           const [count, setCount] = React.useState(0)
           const value = React.useMemo(() => [count, setCount], [count])
          


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

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


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

            так как интерфейс в каждый момент времени — цельная система, а синхронизация состояний — ключевой момент.


            Вам не нужно состояние какой-то отдельной формы на глобальном уровне, зачем синхронизировать то что не должно быть синхронизированным? А если какое-то состояние нужно на глобальном уровне всему приложению — ну тогда мы и оборачиваем соответствующим провайдером все приложение. Такой подход делает состояния иерархическими, мы точно знаем к чему то или иное состояние относиться, а к чему нет — просто видя что тот или иной провайдер оборачивает.

            Что мешает сделать один StateProvider, и внутри компонентов выбирать нужную его часть?


            Ну, так сейчас большая часть Реакт разработчики и живет — ведь это же все тот же Редакс без редакса. При едином стейте вам нужно применять какой-то подход к центральному состоянию, и каким будет этот подход? Скорее всего это будет что-то редаксо-подобное. Если не применять никакого подхода то очень скоро этот стейт превратиться в ад.

            Импортов и кода меньше, возиться с уровнями и отсутствием проброшенного не нужно. А внутри стора можно как угодно скомпоновать логику и методы, удобно разбив по нескольким файлам.


            Идея в том чтобы разделять код с большим соответствием бизнес логике — думать не о том что это редюсер, это экшен и тд, а думать о конкретных стейтах. Это провайдер меню, это провайдер формы и тд, прописывая именно в них нужную логику, а не разделять по разным файлам. Это очень похоже на компонентный подход, но вместо компонентов — стейты. Веб разработка не просто так ушла от разделения на html/css/js к компонентному подходу, где у каждого компонента свои стили, логика и тд.

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

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


            Что значит «обкладывать» — нам нужно заюзать тот же useMemo (или что угодно еще) в одном месте — в файле контекста, и мы оптимизируем это везде где используется этот контекст.

            И да, сам по себе такой подход и приводит к оптимизации — разделяя контексты, вызовы ререндеров будут происходить только при изменении на уровне этого контекста или ниже, Реакт не вызывает ререндер на более высоких уровнях иерархии если изменения произошли на более низких (не важно юзаем мы контексты или проп дриллинг) — нужно стремиться размещать провайдеры/контексты/стейты как можно ниже по иерархии, благодаря чему Реакт, частично, оптимизирует это все сам. А вот если мы запихнем все состояние в один глобальный стейт — у нас начнутся проблемы с оптимизаций. Вот другой мой перевод Кента где подробнее про вопросы оптимизации — habr.com/ru/post/485032
              +2
              Компонентами можно назвать в том числе и обычные html-теги с атрибутами, например <div class=«myclass» onclick=«alert(1)» /> — тут и определенная семантичность в виде предустановленной блочной модели отображения, и стилизация, и методы, и состояние, которое в данном случае вынесено в стилевой класс (например, display: none или положение на странице). Реакт-компоненты выполняют роль агрегатора нескольких таких компонентов с возможностью управлять более крупными сущностями, которые становятся при этом более семантичными + интегрированными в js.

              Но насколько изолированность — это благо? Вот несколько дивов, их стейты находятся в стилевых классах и задают, к примеру, абсолютное положение. Задача — сделать их взаимную раскладку в виде мозаики, причем при наведении на один из элементов он увеличивается и сдвигает остальные. Но так как они независимы и не знают ничего о друг о друге, то это сделать можно только независимыми математическими расчетами в стилях, при этом вбивая отступы в пикселях в качестве констант. А если бы у них был глобальный стейт типа `elements: [{ left, right, width, height }, ...]`, то это можно было бы сделать динамической формулой и гибко задавать условия взаимного расположения и интеракций.

              Точно так же это экстраполируется на более крупные компоненты, и примеры реальных задач, в которых нужно знать о внутренних характеристиках и иметь возможность влиять на них, я привел в комментарии выше. Можно даже по примеру из прошлого абзаца — это будет например список нотификаций или галерея, и по клику на кнопку в другой части страницы нужно закрывать все нотификации или вызывать новые, изменять размеры и положение (сортировку) изображений в галерее. Чтобы наглядней — эта управляющая кнопка далекооо, в компонент Notifications / Gallery ее не поместить.

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

              «Вам не нужно состояние какой-то отдельной формы на глобальном уровне» — в основном как раз нужно.

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

              «Мы точно знаем к чему то или иное состояние относится» — нет, если провайдер оборачивает 10 компонентов, то использоваться может лишь в двух, или вообще в глубоко вложенных. Точно знать в данном случае можно только посмотрев реализацию конкретного компонента, проанализировав, какие контексты он импортит. Иерархичность тут абсолютно бесполезна и приводит лишь к путанице.

              «Применять какой-то подход к центральному состоянию, и каким будет этот подход? Скорее всего это будет что-то редаксо-подобное» — если это касается меня, это будет что-то реактивное и мутабельное.

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

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


                В каких стилях? Можно задавать стили через пропсы, передавая в эти компоненты что нам нужно. Но вообще, в любом случае зачем:

                А если бы у них был глобальный стейт типа `elements: [{ left, right, width, height }, ...]`, то это можно было бы сделать динамической формулой и гибко задавать условия взаимного расположения и интеракций.


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

                Чтобы наглядней — эта управляющая кнопка далекооо, в компонент Notifications / Gallery ее не поместить.
                ну и не помещайте, данный способ не запрещает использовать любой уровень иерархии, включая глобальный.

                Так что «чем более независимыми являются компоненты — тем, как правило, лучше» — однозначно нет, не лучше.


                Документация Реакта: «Компоненты позволяют разбить интерфейс на независимые части, про которые легко думать в отдельности.»

                «Вам не нужно состояние какой-то отдельной формы на глобальном уровне» — в основном как раз нужно.
                Зачем? Конечно такие кейсы возможны, но было бы интересно узнать при каких типовых сценариях это может пригодиться.

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

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

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

                И думать о каждом из них можно как об изолированном блоке, просто не надо думать об иерархии и делать массу импортов.
                Да, так можно делать, но на мой взгляд это просто путь к описанному в статье варианту. Единый стор -> много централизованных сторов -> сторы разнесенные по приложению в соответствии с его бизнес логикой. Ментально это более простой способ мыслить о таком подходе если до этого приходилось работать только с централизованными состояниями.

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

                  +1
                  «Вам не нужно состояние какой-то отдельной формы на глобальном уровне» — в основном как раз нужно.
                  Зачем? Конечно такие кейсы возможны, но было бы интересно узнать при каких типовых сценариях это может пригодиться.

                  1) Сохранение формы (типа комментов) при рефреше

                  2) Сохранение предыдущего состояния формы для «создать ещё» или для обновления дефолтных данных
                    0
                    1) во первых, почему вы хотите юзать для этого именно глобальное состояние? Почему бы не юзать состояние более высокого уровня?

                    Но важно да же не это — если вы хотите сохранять данные юзера, то не надо юзать для этого стейты — они вообще не для этого созданы. Стейт это состояние интерфейса, а не инструмент для сохранения данных.

                    В данном случае — сохранения данных при рефреше логичнее было бы заюзать localStorage браузера.
                    +1
                    «Вам не нужно состояние какой-то отдельной формы на глобальном уровне» — в основном как раз нужно.
                    Зачем? Конечно такие кейсы возможны, но было бы интересно узнать при каких типовых сценариях это может пригодиться.

                    3) Для добавления валидаций — если бэк вернул ошибку, что такой email занят, то нужно записать это в список валидаций данного поля
                    4) При изменении значений в форме можно влиять на другие элементы на странице не событиями form.onChange(values => if (values.fieldValue === 'darkmode') changeWebsiteStyle('darkmode')), так как подобную модель поддерживать сложно и она приводит к «клубку зависимостей», а на глобальном уровне с распространением изменений из стора.

                    В целом мы скорее зашли в тупик, я говорю — что глобальный стейт позволяет избежать иерархий, лишних вложенностей и импортов, а вы в ответ — почему бы не использовать иерархии и оборачивать только те компоненты, в которых эти стейты используются. Я не согласен с тем, что это проще, и приводил массу сценариев, но, похоже, вас не переубедить, поэтому предлагаю остановить дискуссию и пусть читающие ветку комментариев сами оценивают силу аргументов каждой из сторон.
                      0
                      3) Не понятно что вы хотите записывать в список валидаций, при чем на глобальном уровне — если бэк возвращает ошибку, мы просто отображаем в форме эту ошибку. Логика взаимодействия с бэком — в компоненте или в контексте (в контексте можно писать в том числе и асинхронные запросы).

                      4) Если вы говорите про форму которая меняет весь стиль сайта — со светлой темы на темную, например — то да, такой стейт должен быть глобальным, провайдер этого стейта будет оборачивать все приложение так как он оказывает влияние на все приложение.

                        0
                        По поводу хранения состояния форм (валидаторы, значения, функции для проверки каждого поля, характеристики полей) я пришел к тому, что глобальный стейт для этого выгоднее локального, но только при реактивной архитектуре. Взаимодействие с бэком вне слоя апи, а напрямую из компонентов приводит к слишком разбросанной и сложно контролируемой логике. «Просто отображать ошибку» — интересно, каким образом вы планируете это сделать, и как планируете не позволять юзеру отправлять поле с некорректными значениями. Но интересно не очень, так как проектировал системы сложных форм десятки раз, и с помощью внешнего обращения к внутреннему стейту компонентов, и параллельной глобальной системой контроля состояния полей непосредственно через DOM, и двухсторонней связкой по системе событий — у этих способов довольно много недостатков, которые можно устранить именно глобальным реактивным состоянием. Попробуйте, должно понравиться.

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

                0
                Для бэка подойдет

                Да не особо. У бека вполне себе есть единственный источник правды — СУБД. А стейтфул сервера приложений джависты записали в неподдерживаемый говнокод еще во времена Java 5. С переходом на микросерверы у которых СУБД отдельные на микросервис проблема возобновилась, но нормальные люди не переходят на микросервисы на ровном месте, а делают это уже в работающем приложении, где легче выделить относительно самостоятельно работающий компонент.
                  0
                  Тем не менее, бэковые микросервисы не представляют собой единую систему, каждый из них можно переписывать и поддерживать лишь протокол общения. Во фронтенде же все компоненты находятся в едином дом-дереве с общим полем раскладки и пространством стилей, режимом отображения. Мне доводилось создавать системы на изолированных iframe с разных доменов — вот это по факту действительно «микрофронтенды» — но для поддержания общей темы, локализации, размеров, формата, полифиллов и тп пришлось сделать очень толстый АПИ на postMessage для синхронизации. И в то время я как раз и понял, что независимые части в цельной системе — это боль, поэтому сторонился подходов композиции фронта из «как бы независимых» частей, потому что это самообман.

                  На бэке стейтфул независимые сервисы дают массу преимуществ до того момента, как возникает потребность формировать композитные структуры — тогда добавляются дополнительные слои кеширования, промежуточные сборные базы подготовленных данных и вся обвязка с синхронизацией и т.п., так как делать каждый раз по десятку запросов в разные микросервисы становится чрезвычайно накладно, как бы протоколы общения и внутренняя производительность ни совершенствовались. Но это все решается грамотными архитектурами, сохраняя преимущества в виде независимого цикла развития микросервисов и четкой зоны ответственности. В общем, проблемы есть, но их поменьше, чем если делать подобное на фронте
                0
                del
                  0
                  Данный подход напоминает идею мультисторов в effector, где вместо контекстов мы создаем отдельные сторы и по необходимости можем их объединять.
                  Чисто на контексте я б не стал делать — вот причины.
                    0
                    Описанные в приведенной ссылки причины не использовать контексты решаются методом описанном в статье, собсно. В комментариях там тоже есть дискуссия на тему использования разноуровневых стейтов.
                      0
                      Да, и согласен с последней мыслью, что это просто инструменты под разные задачи.
                      Потому я и не стал бы контекстом заменять стейт менеджер.
                    +1
                    При желании можно поменять useState на useReducer…
                    Это дает нам гибкость и уменьшает сложность кода.
                    Подробнее можно про профит от введения dispatch, action type, Reducer?
                    Где уменьшение сложности кода, когда вместо прямого вызова функции-действия вводятся 3 дополнительные сущности?
                    Где увеличение гибкости? Что в архитектуре изменилось в лучшую сторону после введения dispatch, action type, Reducer?

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


                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое