Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Context vs StateManager

По мотивам вопросов чатика react.js@telegram

Оффтоп: пожалуйста, не нужно в сотый раз объяснять уже набившую оскомину тему новичку, который задаст подобный вопрос. Просто поделитесь ссылкой на этот текст. С уважением, Андрей @XaveScor Звёздочка


Краткий ответ на этот вопрос, если вы не хотите разбираться детальнее:

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

После стабилизации контекста и появления хуков в реакте появилась куча людей с одинаковым вопросом: можно ли контекстом заменить реатом/эффектор/редакст/мобыкс/ваш_любимый_стейт_менеджер? И если да, то стоит ли?

Ответ прост: да. Но возникает вопрос: стоит ли?

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

        /-----CHILD_1---------CHILD_1_1
BASE---                    \--CHILD_1_2
       \------CHILD_2

У компонентов CHILD_1_1 и CHILD_1_2 есть общий стейт, который мы храним в CHILD_1, и через контекст пробрасываем его в CHILD_1_1 и CHILD_1_2.

        |Shared State here|    |useContext here|
        |-----------------|    |---------------|     
                 v                 v        |
        /-----CHILD_1---------CHILD_1_1     |
BASE---                    \--CHILD_1_2 <---|
       \------CHILD_2

И всё прекрасно. Мы не тащим лишних зависимостей и у нас всё работает. Но к нас внезапно приходит заказчик и говорит, что ему нужна новая фича. Суть этой фичи опустим, но для её реализации нужно иметь наш shared state в CHILD_2. Что делать? Здесь всё просто:

  1. Ищем компонент A, в котором мы храним shared state.
  2. CHILD_2 - это потомок A?
    • Да. Тогда у нас всё хорошо, просто используем useContext в CHILD_2 и радуемся жизни.
    • Нет. Тогда боль: 0. Ищем общий предок B у A и CHILD_2. 0. Переносим в B всю логику работы с shared state из A. 0. Используем useContext в CHILD_2.

Выглядит просто, не так ли? Возможно, что я уже вам доказал, что ваш_любимый_стейт_менеджер вам не нужен. Но подождите, давайте представим, что у вас не один shared state, а 20; что у вас все shared state хранятся не в одном компоненте, а в 10. В таких условиях схема выше для вас выглядит такой же простой? Для меня нет.

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

Главная проблема при работе с контекстом - это поиск shared state и общего предка. Как нам упростить эти шаги? Так как у нас реакт-дерево - это дерево, то у всех компонентов есть общий предок. Давайте хранить все shared state в root'овом компоненте. И тогда у нас алгоритм проброса shared state будет состоять только из одного пункта:

  1. Просто используем useContext в CHILD_2 и радуемся жизни.

Всё прекрасно, правда? Вроде как да. Теперь давайте рассмотрим как должен выглядеть shared state для удобной работы с ним: Благодаря редаксу в API реакта появился весьма удобный хук React.useReducer, на основе которого можно построить shared state:

const appReducer = (state, event) => {
  return {
    state1: reducer1(state.state1, event),
    state2: reducer2(state.state2, event),
  }
}

const AppStoreProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(appReducer, initialState);

  return(
     <AppDispatchContext.Provider value={dispatch}>
       <AppStateContext.Provider value={state}>
         {children}
       </AppStateContext.Provider>
    </AppDispatchContext.Provider>
  )
}

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

  1. Наше решение очень-очень похоже на редакс. Почему бы не взять его?
  2. В редаксе есть оптимизации, которые позволяют ему работать куда лучше, к примеру combineReducers.ts#L192 - не пересоздаём те части стейта, которые не были изменены
  3. У биндингов к реакту react-redux есть уже готовые хуки по типу useSelector, которые не ререндерят компонент, если стейт не сохранился.
  4. У react-redux есть какие-никакие девтулзы, которые позволяют дебажить приложение.

Беря во внимание как минимум эти 4 пункта я не вижу смысла советовать пились собственное редаксоподобное решение, которое будет ничем не лучше, чем сам редакс во всём, кроме одного пункта: суммарный размер вашего решения будет меньше чем у редакса - это правда. Но в остальном полные минусы, так что не нужно изобретать то, что уже изобретено. У вас стоит задача написать приложение, а не написать лучший стейт менеджер.

Вывод

Не нужно использовать контекст как стейт менеджер

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

Вы хотите уменьшить размер зависимостей?

Если у вас маленький проект, то стоит взять что-то попроще, к примеру, Svelte. Так вы получите очень маленький размер бандла.

А возможно у вас вообще нет динамики, поэтому лучше просто нагенерить статических страниц с помощью Hugo

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

@negamaxi

This comment has been minimized.

Copy link

@negamaxi negamaxi commented Sep 7, 2019

Да, мы изобрели собственный стейт менеджер, который, к тому же, очень сильно будет смахивать на редакс. - мы от силы изобрели собственный Provider-компонент, за все остальное отвечают useReducer и Context API. То есть большая часть "лучшего стейт-менеджера" написана и протестирована за нас разработчиками React. Мы отвечаем всего-лишь за это:

const AppStoreProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(reducer, initialState);

  return(
     <AppDispatchContext.Provider value={dispatch}>
       <AppStateContext.Provider value={state}>
         {children}
       </AppStateContext.Provider>
    </AppDispatchContext.Provider>
  )
}
@XaveScor

This comment has been minimized.

Copy link
Owner Author

@XaveScor XaveScor commented Sep 9, 2019

@negamaxi
А вы уходите в развитие приложения. Покажите как все будет выглядеть, если вам понадобится 30 таких сторов.

@negamaxi

This comment has been minimized.

Copy link

@negamaxi negamaxi commented Sep 10, 2019

@XaveScor если мы пытаеся заменить именно Redux, то масштабирование должно происходит за счет редюсеров (у Redux тоже один стор).

Другое дело, что перейти с Redux на useReducer + Context API и продожать пользоваться глобальным хранилищем это то же самое, что пересесть со скутера на мотоцикл и продолжать ездить 40 км/ч. React предполагает использование разноуровневых локальных состояний, что помогает, например, не заниматься ручной чисткой состояния, как это часто бывает в Redux.

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

@XaveScor

This comment has been minimized.

Copy link
Owner Author

@XaveScor XaveScor commented Sep 11, 2019

@negamaxi
если мы пытаеся заменить именно Redux, то масштабирование должно происходит за счет редюсеров (у Redux тоже один стор).
Именно. И у вас возникает куча провайдеров, которые будут на каждый чих ререндерить дерево, если конечно, вы не будете забивать костыли по типу React.memo, что плодит кучу бесполезного мусора в реакт-дереве.

Другое дело, что перейти с Redux на useReducer + Context API и продожать пользоваться глобальным хранилищем это то же самое, что пересесть со скутера на мотоцикл и продолжать ездить 40 км/ч.
Я говорю, что не надо использовать в принципе useReducer + Context API. Да, если у вас какой-то тестовый провект, где вы экспериментируете, то без проблем, хоть черта лысого можете вызывать.

React предполагает использование разноуровневых локальных состояний, что помогает, например, не заниматься ручной чисткой состояния, как это часто бывает в Redux.
Верно. И это является оень плохой практикой. Как минимум потому что мы используем 100500 различных практик, вместо того чтобы иметь одно унифицированное решение. В пределе, я вообще за отказ от локального состояния внутри реакт компонентов. Отсутствие локального состояния позволит полностью выкинуть пакеты react и react-dom из рантайма и компилировать реактовский компонент в цепочку нативных вызовов. Так, к примеру, делает Svelte.

С другой стороны, организация локальных состояний тем образом, который описан в документации, лишает возможности влиять одним событием на несколько состояний сразу. Исправить этот недостаток можно при помощи всплывающих событий.
Вместо одного универсального решения мы изобретаем ещё один костыль, который исправляет одну из 100500 проблем.

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

@negamaxi

This comment has been minimized.

Copy link

@negamaxi negamaxi commented Sep 12, 2019

@XaveScor
Именно. И у вас возникает куча провайдеров... - если нужно заменить именно Redux то нужен один единственный провайдер с useReducer внутри, который будет играть роль глобального хранилища. Масштабирование происходит за счет редюсеров:

const appReducer = (state, event) => {
  return {
    state1: reducer1(state.state1, event),
    state2: reducer2(state.state2, event),
  }
}

const AppStoreProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(appReducer, initialState);

  return(
     <AppDispatchContext.Provider value={dispatch}>
       <AppStateContext.Provider value={state}>
         {children}
       </AppStateContext.Provider>
    </AppDispatchContext.Provider>
  )
}

куча провайдеров, которые будут на каждый чих ререндерить дерево - это значит, что провайдер неправильно приготовлен. Если коротко - дочернее дерево надо передавать в провайдер состояния в виде готовой React-ноды (через props.children). В таком случае перерисовываться будут только консюмеры.

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

Насчет compile time оптимизации мне сказать нечего, но непонятно, каким образом отказ от локального состояния поможет избавиться от react в рантайме, если этот пакет используется в байндингах к внешним менеджерам состояний. И вообще я сомневаюсь, что эту проблему нельзя решить, учитывая, что в Facebook разрабатывают утилиту с похожим на svelte поведением.

Вместо одного универсального решения мы изобретаем ещё один костыль, который исправляет одну из 100500 проблем. - универсальных решений не существует, существуют решения для конкретных задач. Об этом я бы и хотел прочитать в новом варианте гиста, о сравнении слабых и сильных сторон с учетом use cases. А пока что да, не убедительно.

@XaveScor

This comment has been minimized.

Copy link
Owner Author

@XaveScor XaveScor commented Sep 12, 2019

@negamaxi

Ниже будет не критика ответа, а небольшое раскрытие мысли.

Масштабирование происходит за счет редюсеров:

Ок, я вас понял. Здесь я могу добавить только ссылку на древний ишшью: facebook/react#13739. А так - да, правда усчитывая какой combineReducers весёлый у редакса, то придётся чуть ли не копировать его.

Но не вижу, каким это образом говорит именно в пользу Redux.

В пользу Редакса или любого другого стейт менеджера говорит лишь унифицированность подхода. Вам не нужно думать ни о всплытии состояния, ни о том где находится очередное локальное состояние. Всё просто: Реакт - шаблонизатор, Стейт-менеджер - логика.

@negamaxi

Насчет compile time оптимизации мне сказать нечего

Давайте пока опустим эту тему. Пока моя мысль такая: выкинув из семантики всё, кроме jsx и определение компонента(props => JSX), то оставшееся можно статически преобразовывать в цепочку нативных функций. С внешними зависимостями да, нужно думать. Но это огромная непаханная тема пока что. Как минимум для меня.

универсальных решений не существует, существуют решения для конкретных задач

В данном случае, существует. Но к этому давайте подойдём в новом варианте гиста.

@XaveScor

This comment has been minimized.

Copy link
Owner Author

@XaveScor XaveScor commented Sep 20, 2019

@negamaxi Спасибо за отзыв. Обновил статью.

@dmitryshelomanov

This comment has been minimized.

Copy link

@dmitryshelomanov dmitryshelomanov commented Oct 28, 2019

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

Плюсы

  • Ресет с помощью реакта
  • Не нужно сторонних либ и всяких реселектов
  • Простота тестирования

Минусы

  • Захламленные девтулзы

  • Очень жесткая связанность с реактом (очень большой минус. Можно не плохо напороться на это)

    Думаю этот минус нужно расписать подробнее. Поинт в том что эпик нельзя пересоздавать. А если он ожидает что то на вход ? Сервис (апи, какой то стейт, не важно) то получив его эпик пересоздастся и ты потерчешь старые данные и какие то фетчи. Но думаю это можно решить (с rx просто).

  • Нужно писать что то для обработки сайд эффектов

export const initialState = { counter: 0 };

function createActions(dispatch) {
  function increment() {
    dispatch({ type: 'INCREMENT' });
  }

  function decrement() {
    dispatch({ type: 'DECREMENT' });
  }

  return { increment, decrement };
}

const CounterStateContext = React.createContext(initialState);
const CounterActionsContext = React.createContext();

function reducer(state, action) {
  return state
}

// сайд эффект
function createRootEpic() {
  function store(state$) {
    return state$.pipe(
      ofType('INCREMENT'),
      tap(() => {
        // store
      })
    )
  }

  return combineEpics([store])
}

function useCounterFacade() {
  const rootEpic = createRootEpic()
  const [state, dispatch] = useEpicsReducer(
    reducer,
    initialState,
    rootEpic,
  );

  const actions = useMemo(
    () => createActions(dispatch),
    [dispatch],
  );

  return [state, actions]
}

function CounterController({ children }) {
  const [state, actions] = useCounterFacade();

  return (
    <CounterStateContext.Provider value={state}>
      <CounterActionsContext.Provider value={actions}>
        {children}
      </CounterActionsContext.Provider>
    </CounterStateContext.Provider>
  );
}

PS
Я не призываю юзать контекст вместо стейт менеджера и не говорю что что то хуже и что то не нужно юзать. Вы можете использовать то что больше нравится вам и подходит под проект. (Возможно это не большой проект с минимум логики). На данный момент пишу проект в котором куча контекста (возможно будет переделываться. Проект большой)

И не думаю что всем нужно говорить что контекст не юзабелен и тд, нужно тому кто собирается что то написать - прикинуть минусы и плюсы и выбрать

Хотите огрести осознано - берите контекст
И если попробовать - пробуйте
Но если не понимаете сути - берите стейт мменеджер

@XaveScor

This comment has been minimized.

Copy link
Owner Author

@XaveScor XaveScor commented Nov 1, 2019

@userbq201
Ваши плюсы работают пока у вас только 1 провайдер и конъсьюмер. Как только вы хотите иметь несколько стейтов начинаются проблемы, которые я описал в тексте:
Ресет с помощью реакта: Это удобно пока у вас каждый провайдер отвечает за свою единицу стейта. И с ростом приложения вы будете натыкаться на то, что у вас все провайдеры будут всё ближе и ближе в корню дерева компонентов. Этот процесс я и описал в статье;
Не нужно сторонних либ и всяких реселектов: конечно. Вам придётся всё самостоятельно написать и протестировать. Весело же вместо написания бизнес-фич пилить собственное редаксоподобное решение;
Простота тестирования: По сравнению с чем? Я не могу придумать ни одной ситуации в которой тестирование приложения с редаксом было сложнее, чем тестирование приложения с использованием контекста.

@dmitryshelomanov

This comment has been minimized.

Copy link

@dmitryshelomanov dmitryshelomanov commented Nov 5, 2019

Ты не прав. Тестирую ридакс нужно все замокать (redux mock store), а мой поинт как раз в том что контексты бить на сущности - они выходят не зависимые, тестировать не зависимо куда проще

И как говорил на любом уровне держатся эти плюсы
Но и минусы тоже (как и потребность написать свой код для этог)

@XaveScor

This comment has been minimized.

Copy link
Owner Author

@XaveScor XaveScor commented Nov 16, 2019

@userbq201 Если мне не изменяет память, то редакс тоже позволяет бить на сущности. combineReducers для этого и придумали)
Ещё какие аргументы?)

@it-xp

This comment has been minimized.

Copy link

@it-xp it-xp commented May 16, 2020

Интересный гист, но вообще это как спорить что важнее в теле, руки, или ноги?

Использовать надо и то и то, просто для разных целей.

Я не стану расписывать прям все юзкейсы, но поверхностно пробегусь

Зачем использовать стейт менеджер? Конечно же для реализации бизнес логики конкретного приложения

Зачем использовать контекст? например если надо разработать сложный реюзабельный компонент.

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

И вот тут начинаются пляски с пританцовками, так повернёшь - бесполезное дублирование данных, как следствие рассадник сложноуловимых багов, а по другому, адская логика, по этому вопрос стоит скорее не что вместо чего использовать, а как подружить так, что бы и то и то работало без лишних "мама роди меня обратно"

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

@dmitryshelomanov

This comment has been minimized.

Copy link

@dmitryshelomanov dmitryshelomanov commented Jul 9, 2020

@userbq201 Если мне не изменяет память, то редакс тоже позволяет бить на сущности. combineReducers для этого и придумали)
Ещё какие аргументы?)

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

и как раз отвечу

да позволяет
но тут есть большое НО
ты аффектишь все подписчики
в то время как контекст не реагирует на это

каждый контекст обособленный

@dmitryshelomanov

This comment has been minimized.

Copy link

@dmitryshelomanov dmitryshelomanov commented Jul 9, 2020

@dmitryshelomanov

This comment has been minimized.

Copy link

@dmitryshelomanov dmitryshelomanov commented Jul 9, 2020

и да посоны
берем эффектор и понимаем что редакс и контексты не нужны)

@budarin

This comment has been minimized.

Copy link

@budarin budarin commented Oct 22, 2020

  • да не согласен я с ним!
  • с кем?
  • да с обоими!
    :)

веселопеды нужны!
веселопеды важны!
без них нет движения вперед :)

https://www.npmjs.com/package/@budarin/use-react-redux

использую на проекте и не страдаю - все фишки redux включая производительность кроме devtools (я ими и в редаксе не особенно пользовался)

@KaraMokusee

This comment has been minimized.

Copy link

@KaraMokusee KaraMokusee commented Feb 25, 2021

Но к нас внезапно приходит заказчик и говорит, что ему нужна новая фича.

опечаточка

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment