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 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 commented Sep 9, 2019

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

@negamaxi

This comment has been minimized.

Copy link

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 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 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 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 commented Sep 20, 2019

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

@userbq201

This comment has been minimized.

Copy link

userbq201 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 commented Nov 1, 2019

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

@userbq201

This comment has been minimized.

Copy link

userbq201 commented Nov 5, 2019

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

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

@XaveScor

This comment has been minimized.

Copy link
Owner Author

XaveScor commented Nov 16, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.