Как эффективно применять React Context

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

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

Рекомендую перед прочтением этой статьи прочитать Как управлять состоянием React приложения без сторонних библиотек. Особенно обратите внимание на рекомендацию о том что не нужно применять контексты для решения всех проблем связанных с передачей состояния. Однако, когда вам все таки понадобиться применять контексты, надеюсь эта статья поможет вам делать это наиболее эффективным образом. Также помните о том что контексты НЕ обязательно должны быть глобальными, контекст может (и скорее всего, должен) быть частью какой либо ветки в структуре вашего приложения.

Давайте, для начала, создадим файл src/count-context.js и пропишем там наш контекст:

// src/count-context.js
import React from 'react'

const CountStateContext = React.createContext()
const CountDispatchContext = React.createContext()

Во-первых, в CountStateContext нет начального значения. Его можно было бы прописать, например, так: React.createContext ({count: 0}), но в этом нет смысла. Дефолтное значение defaultValue будет полезно только в такой ситуации:

function CountDisplay() {
  const {count} = React.useContext(CountStateContext) // <-
  return <div>{count}</div>
}

ReactDOM.render(<CountDisplay />, document.getElementById('⚛️'))

Из-за того что у CountStateContext нет значения по умолчанию, мы получим ошибку в useContext Это из-за того что наше дефолтное значение не было определено, оно undefined а мы не можем передавать undefined в useContext.

Никому не нравятся runtime-ошибки, так что, скорее всего, вашей первой реакцией будет добавление какого-то дефолтного значения. Но зачем вообще использовать контекст если вы не используете его для какого-то реального значения? Если он будет использовать значение по умолчанию, то в нем едва ли есть какой либо смысл. В подавляющем числе случаев когда вы создаете и используете контекст в своем приложении вы хотите чтобы потребители (Context Consumer), которые используют useContext, отображались внутри провайдера (Context Provider), который может передать какое-то полезное значение.

Замечу, что да, возможны ситуации когда значения по умолчанию полезны, но в большинстве случаев они не нужны.

Документация Реакта говорит о том что передача дефолтных значений "может быть полезной для тестирования компонентов в изоляции без необходимости оборачивать их". Да, так делать можно, но, на мой взгляд, лучше оборачивать компоненты нужным контекстом. Помните - вы не можете быть уверены в тесте на сто процентов если делаете в нем что-то, чего не делаете в самом приложении. Существуют ситуации когда нужно так делать The Merits of Mocking, но это не тот случай.

Примечание. Если вы используете Flow или TypeScript, и применяете React.useContext, отсутствие дефолтного значения может сильно раздражать. Ниже я покажу как избежать этой проблемы.

Так зачем нужен этот CountDispatchContext? Уже какое-то время я экспериментирую с контекстами, я так же общаюсь со знакомыми из Facebook которые экспериментируют с ними намного дольше чем я, и могу сказать что самая простая вещь которую вы можете сделать для того чтобы избежать проблем с контекстами (особенно если вы вызываете dispatch в эффектах) это разделить состояние (state) и dispatch в контексте. Это звучит странно, но сейчас я все объясню!

Кастомный компонент провайдер

Чтобы модуль контекста работал, нам нужно использовать Provider и компонент, который передает значение. Пример:

function App() {
  return (
    <CountProvider>
      <CountDisplay />
      <Counter />
    </CountProvider>
  )
}

ReactDOM.render(<App />, document.getElementById('⚛️'))

Итак, давайте напишем код этого компонента:

// src/count-context.js
import React from 'react'

const CountStateContext = React.createContext()
const CountDispatchContext = React.createContext()

function countReducer(state, action) {
  switch (action.type) {
    case 'increment': {
      return {count: state.count + 1}
    }
    case 'decrement': {
      return {count: state.count - 1}
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`)
    }
  }
}

function CountProvider({children}) {
  const [state, dispatch] = React.useReducer(countReducer, {count: 0})
  return (
    <CountStateContext.Provider value={state}>
      <CountDispatchContext.Provider value={dispatch}>
        {children}
      </CountDispatchContext.Provider>
    </CountStateContext.Provider>
  )
}

export {CountProvider}

Примечание. Это довольно надуманный пример. Я намеренно переусложнил решение чтобы показать более приближенный к реальности код. Это не значит что всегда нужно так делать. Применяйте хук useState если это больше подходит для вашей ситуации. На практике, какие-то ваши провайдеры будут простыми и короткими, как этот, а другие будут КУДА более сложными, и будут применять множество различных хуков.

Кастомный хук потребитель (Consumer Hook)

Обычно разработчики используют контексты таким образом:

import React from 'react'
import {SomethingContext} from 'some-context-package'

function YourComponent() {
  const something = React.useContext(SomethingContext)
}

Но на мой взгляд существует куда более удобный способ:

import React from 'react'
import {useSomething} from 'some-context-package'

function YourComponent() {
  const something = useSomething()
}

Чем таком подход лучше? Ну, это открывает нам целый ряд новых возможностей:

// src/count-context.js
import React from 'react'

const CountStateContext = React.createContext()
const CountDispatchContext = React.createContext()

function countReducer(state, action) {
  switch (action.type) {
    case 'increment': {
      return {count: state.count + 1}
    }
    case 'decrement': {
      return {count: state.count - 1}
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`)
    }
  }
}

function CountProvider({children}) {
  const [state, dispatch] = React.useReducer(countReducer, {count: 0})
  return (
    <CountStateContext.Provider value={state}>
      <CountDispatchContext.Provider value={dispatch}>
        {children}
      </CountDispatchContext.Provider>
    </CountStateContext.Provider>
  )
}

// наши кастомные хуки:

function useCountState() {
  const context = React.useContext(CountStateContext)
  if (context === undefined) {
    throw new Error('useCountState must be used within a CountProvider')
  }
  return context
}

function useCountDispatch() {
  const context = React.useContext(CountDispatchContext)
  if (context === undefined) {
    throw new Error('useCountDispatch must be used within a CountProvider')
  }
  return context
}

export {CountProvider, useCountState, useCountDispatch}

Во-первых, кастомные хуки useCountState и useCountDispatch используют React.useContext для того чтобы получить значение контекста из ближайшего CountProvider. В ситуации когда значения нет, мы показываем сообщение об ошибке, которое указывает на то что хук должен быть использован внутри CountProvider. Так как это ошибка, то, конечно, полезно эту ошибку отобразить.

Кастомный компонент потребитель

Если у вас есть возможность использовать хуки, то вы можете пропустить этот раздел. Однако, если вы используете React < 16.8.0, или, если вам нужно использовать Контекст в классовом компоненте, вот каким образом вы можете поступить, используя подход render-prop:

function CountConsumer({children}) {
  return (
    <CountStateContext.Consumer>
      {context => {
        if (context === undefined) {
          throw new Error('CountConsumer must be used within a CountProvider')
        }
        return children(context)
      }}
    </CountStateContext.Consumer>
  )
}

Именно так я и делал до того как появились хуки. Это не плохой подход. Однако, если вы можете использовать хуки, используйте хуки.

TypeScript / Flow

Выше я обещал рассказать о том как избежать ошибок связанных с тем что мы не стали указывать дефолтное значение (defaultValue) при использовании TypeScript или Flow. Вот оно:

// src/count-context.tsx
import * as React from 'react'

type Action = {type: 'increment'} | {type: 'decrement'}
type Dispatch = (action: Action) => void
type State = {count: number}
type CountProviderProps = {children: React.ReactNode}

const CountStateContext = React.createContext<State | undefined>(undefined)
const CountDispatchContext = React.createContext<Dispatch | undefined>(
  undefined,
)

function countReducer(state: State, action: Action) {
  switch (action.type) {
    case 'increment': {
      return {count: state.count + 1}
    }
    case 'decrement': {
      return {count: state.count - 1}
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`)
    }
  }
}

function CountProvider({children}: CountProviderProps) {
  const [state, dispatch] = React.useReducer(countReducer, {count: 0})
  return (
    <CountStateContext.Provider value={state}>
      <CountDispatchContext.Provider value={dispatch}>
        {children}
      </CountDispatchContext.Provider>
    </CountStateContext.Provider>
  )
}

function useCountState() {
  const context = React.useContext(CountStateContext)
  if (context === undefined) {
    throw new Error('useCountState must be used within a CountProvider')
  }
  return context
}

function useCountDispatch() {
  const context = React.useContext(CountDispatchContext)
  if (context === undefined) {
    throw new Error('useCountDispatch must be used within a CountProvider')
  }
  return context
}

export {CountProvider, useCountState, useCountDispatch}

При таком подходе можно использовать useCountState или useCountDispatch без проверки на undefined, так мы уже провели эту проверку!

Вот версия на codesandbox

Что на счет type типов в dispatch?

В этот момент пользователи Редакса могли бы закричать: "Эй, а где генераторы экшенов?!" (Action Creator). Если вам хочется, то вы можете применять генераторы экшенов, в этом нет ничего плохого. Но на мой взгляд это лишняя абстракция, в которой нет необходимости. К тому же, если вы используете TypeScript или Flow, и вы тщательно прописали типы для ваших экшенов, то вам не нужно прописывать генераторы экшенов. Вы уже и так получаете автодополнение и отображение ошибок типов в редакторе кода:

Мне действительно нравиться передавать dispatch таким образом, и, как дополнительный плюс, dispatch стабилен все время пока живет компонент который создал его, так что его можно смело добавлять в лист зависимостей useEffect (не имеет значения добавлен он туда или нет).

Если вы не типизируете свой JavaScript код (вообще, неплохо было бы типизировать), то ошибка которую мы отображаем при пропущенном типе экшена - отказоустойчива.

Что на счет асинхронности?

Это хороший вопрос. Что делать если нужно сделать асинхронный запрос, и вам нужно отправлять (dispatch) данные в ходе этого запроса? Да, это можно сделать в самом вызываемом компоненте, но получается придется прописывать это все для каждого компонента.

Я предлагаю сделать вспомогательную функцию внутри вашего контекстного модуля, эта функция будет принимать dispatch вместе с любыми другими данными которые вам нужны. Вот пример такой функции (из моего курса Advanced React Patterns Workshop):

// user-context.js
async function updateUser(dispatch, user, updates) {
  dispatch({type: 'start update', updates})
  try {
    const updatedUser = await userClient.updateUser(user, updates)
    dispatch({type: 'finish update', updatedUser})
  } catch (error) {
    dispatch({type: 'fail update', error})
  }
}

export {UserProvider, useUserDispatch, useUserState, updateUser}

После чего вы можете использовать ее так:

// user-profile.js
import {useUserState, useUserDispatch, updateUser} from './user-context'

function UserSettings() {
  const {user, status, error} = useUserState()
  const userDispatch = useUserDispatch()
  function handleSubmit(event) {
    event.preventDefault()
    updateUser(userDispatch, user, formState)
  }

  // more code...
}

Совмещение состояния (state) и отправки (dispatch)

Некоторые считают такой код излишним:

const state = useCountState()
const dispatch = useCountDispatch()

Они спрашивают, "можно ли просто делать так?":

const [state, dispatch] = useCount()

Да, можно:

function useCount() {
  return [useCountState(), useCountDispatch()]
}

Итоги

Финальная версия кода:

// src/count-context.js
import React from 'react'

const CountStateContext = React.createContext()
const CountDispatchContext = React.createContext()

function countReducer(state, action) {
  switch (action.type) {
    case 'increment': {
      return {count: state.count + 1}
    }
    case 'decrement': {
      return {count: state.count - 1}
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`)
    }
  }
}

function CountProvider({children}) {
  const [state, dispatch] = React.useReducer(countReducer, {count: 0})
  return (
    <CountStateContext.Provider value={state}>
      <CountDispatchContext.Provider value={dispatch}>
        {children}
      </CountDispatchContext.Provider>
    </CountStateContext.Provider>
  )
}

function useCountState() {
  const context = React.useContext(CountStateContext)
  if (context === undefined) {
    throw new Error('useCountState must be used within a CountProvider')
  }
  return context
}

function useCountDispatch() {
  const context = React.useContext(CountDispatchContext)
  if (context === undefined) {
    throw new Error('useCountDispatch must be used within a CountProvider')
  }
  return context
}

export {CountProvider, useCountState, useCountDispatch}

Вот код на codesandbox

Заметьте что я НЕ экспортирую CountContext. Это сделано намеренно. Благодаря этому существует только один способ предоставлять значение контекста и только один способ потреблять его. Это позволяет убедиться в том что разработчики используют значение так как это было задумано, это также позволяет добавлять полезные утилиты для потребителей.

Надеюсь это было полезно для вас! Помните:

  1. Не нужно сразу же хвататься за контексты при решении любой проблемы связанной с передачей состояний.

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

  3. Вы можете (и скорее всего должны) иметь несколько логически не связанных контекстов в вашем приложении.

Средняя зарплата в IT

111 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 7 268 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

    +3

    В который раз убеждаюсь, что Контексты с Хуками в сыром виде совершенно не пригодны для работы. Для этого нужно написать свой мини-фреймворк (как у вас), задать границы применения, добавить щепотку мемоизации… И только тогда можно пользоваться этим без боли.


    Вопрос, только, а зачем городить свой велосипед? (Вопрос не к автору статьи, а, скорее, риторический).


    Можно взять Recoil, Mobx, ну или Redux-tookit на худой конец.

      +1
      Реакт в сыром виде не пригоден для работы, придется написать свой мини-фреймворк. Зачем городить велосипед, можно взять angular/ember ну или backbone на худой конец.
        0
        backbone

        А чем Backbone вам поможет? Это же ещё больший конструктор.

          0
          Реакт нинужен.
          –1
          Пошёл на фейсбук посмотреть используют ли они контекст. Используют во-всю. В статье приведены примитивные примеры, для них не нужно разделения на отдельные контексты, потому что «React — быстр». Вполне нормально, что что-то там «перерендрится», до тех пор пока не возникают «тормоза». Мы делали «бесконечную древовидную таблицу» с раскрывашками, возможностью отметить строки, да так чтобы отмеченная родительская строка отмечала всё дочерние и бесконечной подгрузкой дочерних данных, там контекст не подходит потому, что там вообще ничего не подходит. Не нужно делать такие компоненты, хоть и очень хочется. Даже если пихать всё в react-window получается, что нужно пересчитывать дерево (бесконечное) на каждый чих и это большая печаль. А без react-window всё страшно тормозит с любым стейт-менеджером.
            +1

            Virtual-scroll компоненты (если делать их по уму, а не как в $mol) — это всегда большое количество подводных камней, костылей и прочего. Особенно когда содержимое это дерево произвольного размера элементов. Особенно когда часть из них могут иметь height на десятки тысяч пикселей. Особенно когда нужно чтобы это всё было редактируемым. Ах да, ещё же поиск :)

          +3

          Пожалуй стоит упомянуть ещё про пару моментов, которые можно было бы назвать западнёй:


          1) Не пишите в коде вещи вроде


          <context.Provider value={{ dispatch, state }}/>

          Причина: { dispatch, state } — это всегда новый объект. А там внутри что-то вроде:


          if (oldValue !== prevValue) notifyConsumers();

          Это в свою очередь reset-ит context value при каждом рендере компонента, который использует Provider. А это в свою очередь принудительно rerender-ит всех тех кто использует useState.


          Отсюда вывод — нужно группировать несколько значений в одно каждый render? useMemo к вашим услугам


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


          3) При таком вот подходе нельзя подписаться только под изменение какого-то конкретного поля в state, т.к. любое изменение state вызывает rerender всех потребителей контекста. Всегда без исключения. И кастомный хук не поможет, т.к. React пока не умеет в хуки оторванные от render-а.


          Это причина того, почему тот же самый redux не использует context api для хранения state. Вместо этого там своя модель подписки. Рекомендую посмотреть как устроен useSelector. В свою очередь это создаёт проблемы при использовании redux с какими-нибудь другими штуками вроде react-router-а, который как раз использует контекст для таких вещей. Они обновляются столь не синхронизировано, что бывают очень бесячие баги из области race condition.


          По сути context api пока выглядит очень куцым, недоделанным. Оно слишком топорное и с его помощью невозможно реализовать хоть сколько нибудь сложные вещи. Но кажется там у них идёт работа над его улучшением.

            –2

            пункт 3) не актуален в связи с наличием не документированных возможностей
            https://medium.com/@vadim_budarin/redux-на-react-hooks-react-сontext-ad673192309b

              +1
              даже наивная реализация React Redux на “чистых” hooks+context не уступает сколько-нибудь значимо в production mode пакету react-redux

              Это не так. Я бы даже сказал что это чепуха. Вы занимаетесь самообманом. На многих реальных приложениях разница будет просто аховой. Объяснить или вы сами всё хорошо понимаете? (но тогда зачем написали это ^)


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

              useMemo это капля в море и можно вообще исключить из уравнения. Даже пару vDom-тегов лишний раз создать и вы уже перебороли useMemo. Там ведь внутрь несколько сравнений указателей. ЗНАЧИТЕЛЬНО меньшая производительность будет именно из-за лишних рендеров. Т.е.:


              • вызываются все хуки
              • вызываются все эффекты
              • генерируется куча новых vDom элементов
              • производится множество реконсиляций
              • если где-то не было мемоизации аля React.memo — оно ещё и в глубину убегает

              Вот именно это ^ создаёт тормоза в случае "топорного" решения. Которое СИЛЬНО медленнее. Умоляю, выкиньте тот абзац из статьи. Ваш результат на табличке скорее всего базируется на том, что по условиям задачи происходит total-rerender всякий раз. И любая мемоизация просто оказывается лишней. Я прав? (лень лезть в код задачи)


              Посмотрите — js занимает незначительное время в обновлении дерева — основное время занимает layout и paint.

              Что делает само сравнение бессмысленным. Вы зачем его провели то? ) Можно вообще выкинуть всё и оставить одни props.


              мы сегодня уже используем данное решение в связке с use-context-selection в продукте

              Ну я залез туда… И что я там увидел? Да ровным счётом то же самое, что и в redux-е. Свой observable в обход контекста. Со всеми вытекающими проблемами. Т.е. чтобы переписать redux, надо… написать redux? :) В чём выгода?


              будущие версии React обещают оптимизацию useContext (useContextSelector и lazy Context propagation), что уравняет ее в производительности с react-redux

              Вот их и ждём. Всё так.


              А что за недокументированные возможности? unstable_batch?


              P.S. ваши mapStateToProps и mapDispatchToProps можно сильно оптимизировать сделав props опциональной зависимостью (ибо.length === 2).

                –1
                1) сначала прочтите те ссылки что указаны в статье потом начнем что-то обсуждать
                когда вы прочтете большой тред Марка Эриксона (маинтейнер redux) по поводу будущих путей развития — можете возвращаться к продолжению диалога :)

                2) удосужьтесь запустить профилировщик для этих примеров и на их примере покажите мне ошибку в моих рассуждениях

                3) прочтите www.npmjs.com/package/use-context-selection

                4) меньше патетики и предположений — используйте больше практики!
                я это решение вдоль и поперек профилировал и каждый оператор тестировал — у вас одни эмоции и ни капли кода и подтверждения

                про PS: не понял каким образом опциональность mapStateToProps и mapDispatchToProps поможет оптимизации? оптимизации чего?
                  +1

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


                  3) прочтите www.npmjs.com/package/use-context-selection

                  Я залез в исходный код и даже привёл вам ссылку на то как оно работает изнутри. Если вам лень её открыть, то вот суть этого пакета:


                  function createContextDispatcher<T>(
                    listeners: Set<ContextListener<T>>,
                    equalityFn: EqualityFn<T> = isEqualShallow
                  ): ContextComparator<T> {
                    return (oldValue: T, newValue: T): 0 => {
                      for (const listener of listeners) {
                        const newResult = listener.selection(newValue);
                  
                        if (!equalityFn(newResult, listener.selection(oldValue))) {
                          listener.forceUpdate(newResult);
                        }
                      }
                  
                      return 0;
                    };
                  }

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


                  я это решение вдоль и поперек профилировал и каждый оператор тестировал — у вас одни эмоции и ни капли кода и подтверждения

                  ? я выше привёл ряд аргументов. Вам есть что сказать то? Или какие-то из моих аргументов вам непонятны?


                  Касательно профилирования — вы профилировали render-движок браузера. Зачем?!

                    0
                    >Ну я залез туда… И что я там увидел? Да ровным счётом то же самое, что и в redux-е. Свой observable в обход контекста. Со всеми вытекающими проблемами. Т.е. чтобы переписать redux, надо… написать redux? :) В чём выгода?

                    вы не внимательно смотрели:
                    use-context-selection =>
                    This library makes use of a non-documented feature available on React.createContext API which allows us to disable dispatching updates to every component accessing a Context value. Then, thanks to hooks, we can dispatch updates specifically to the components listening for some changes.

                    в redux стейт хранится вне контекста исполнения React и там намного сложнее механизм обновления стейта (чего только стоят профессиональные try catch при обновлениях для детектирования проблемы zombie children и последующие форсажные реобновления :) )

                    выгода
                    — в размере решения — TTI сегодня имеет большое значение! 60ms на хорошем 3G при 1й загрузке никому не будут лишними :)
                    — в том, что это решение является верным и возможно будет в redux когда выйдет React concurrent mode — об этом в roadmap (когда будет утвержден пропозал к useContext с селектором) там обсуждается еще один вариант — нового хука подписки на сторонние мутируемые объекты
                    — стейт хранится и изменяется в контексте React — корректно работает в concurrent mode
                    — отсутствие zombie children порождаемых redux
                    — возможность более гибко применять (можно проще создавать страничные хранилища и хранилища больших форм)
                    — и все это при той же функциональности и производительности что и в redux

                    и чем это плохо??

                    >Касательно профилирования — вы профилировали render-движок браузера. Зачем?!

                    а что же по вашему я должен профилировать?? сферического коня в вакууме который ни на что не влияет?
                    я профилировал всю связку redux-react-браузер! если я нажимаю кнопку добавить узел в дерево при этом срабатывает вся цепочка — как я могу профилировать только рендер?
                    если вам удается при профилировании профилировать только render — поделитесь как!)

                    поиграйтесь с примером TreeView — там дерево с 1000 элементами — все ваши предположения там проверить можно!

                    вот этот абзац выкиньте пожалуйста пока не попрофилируете примеры! это противоречит даже вашему открытия обсервера!
                    срабатывает обновление лишь того компонента у которого селектор для контекста дал отличный результат от предыдущего!

                    >useMemo это капля в море и можно вообще исключить из уравнения. Даже пару >vDom-тегов лишний раз создать и вы уже перебороли useMemo. Там ведь внутрь >несколько сравнений указателей. ЗНАЧИТЕЛЬНО меньшая производительность >будет именно из-за лишних рендеров. Т.е.:

                    спасибо за замечание про опциональные mapState — учту

                    >Вот у вас в коде только 2-ая. И поэтому вы добавляете props в dependencies для useMemo. А это очень сильно бьёт по производительности в ряде случаев когда эти самые state не зависят от props. И там элементарная вилка вида:

                    if (mstp.length === 2) return mstp(state, ownProps);
                    else return mstp(state);

                    P:S: спорная политика хабра не дает возможности оперативно отвечать :)
                      0
                      const Context = React.createContext<T>(initValue, createContextDispatcher<T>(listeners, equalityFn));

                      О. Нашёл наконец, про что вы говорили. Спасибо. Я правда ожидал большего, думал там что-то вроде частичной подписки на содержимое контекста.


                      стейт хранится и изменяется в контексте React — корректно работает в concurrent mode

                      Это хорошо.


                      возможность более гибко применять (можно проще создавать страничные хранилища и хранилища больших форм)

                      Ничего не понял :) Вы про множественные stores?


                      и чем это плохо??

                      По большому счёту ничем (кроме "any"). Но проблемы у этого решения, те же что и у redux. Оно работает в обход react-а. Свой собственный observer и setState-ы в цикле. Значит будут те же самые race-condition-ы с другими компонентами, которые используют уже свои контексты. Например у меня много таких было с react-router-м.


                      Но пока у нас нет нативной поддержки со стороны React выбора у нас всё равно нет. Только такие вот костыли.


                      а что же по вашему я должен профилировать?? сферического коня в вакууме который ни на что не влияет?

                      Код который вы сравниваете. Причём в тех условиях которые предполагаются. Какой самый худший сценарий для "прямолинейного решения"? Когда подписок много и 99% из них зря. Условно вы поменяли кол-во like-ов к комментарию. В идеально только 1 условный <CommentLike/> должен дойти до return <div/>. Если дойдут все (скажем 5_000) комментариев, то производительность в сравнении с оптимизированной версией будет вообще ни разу не сопоставима.


                      А что вы протестировали? Рендер движок? Ну ок, один и тот же рендер движок имеет одну и ту же производительность. Эка новость.


                      срабатывает обновление лишь того компонента у которого селектор для контекста дал отличный результат от предыдущего!

                      Контекст содержит весь store.state. Целиком. Т.е. "отличный результат от предыдущего" это любое изменение стора. Но я полагаю, что вы имеете ввиду именно селекторы из mapStateToProps или mapDispatchToProps. В таком случае это не так.


                      На всякий случай хочу уточнить, мы с вами обсуждаем одно и то же? Я обсуждаю прямолинейную реализацию, тот код в статье. Т.е. БЕЗ use-context-selection. Обычный { createContext, useContext } из react.


                      Так вот, то обновление получают те компоненты которые используют useConnect. И они получают их просто исходя из того, что:


                      <StateContext.Provider value={state}>

                      и


                      const state = useContext(StateContext);

                      Все useMemo уже не играют никакой большой роли, т.к. компонент уже получил render.


                      Вот ваш код:


                      const Container = (ownProps) => {
                        const props = useConnect(
                           mapStateToProps, mapDispatchToProps, ownProps
                        );
                        return {
                          const { propA, propB } = props;
                      
                          return <Component propA={propA} propB={propB} />;
                        };
                      });

                      Сколько в useConnect вы useMemo не используйте, если useConnect уже запущен (а он запущен ввиду useContext), то return <Component propA={propA} propB={propB} /> уже не избежать. Ведь это hook. Он сам часть компонента. Рендер hook-а производится в рамках рендера его родительского компонента.


                      Или же вы предполагали этот пример показать как HoC аналогичный connect, а не как самостоятельный конечный компонент с бизнес-логикой? Если это так, то я вас просто не понял (но зачем тогда useConnect?).


                      если вам удается при профилировании профилировать только render — поделитесь как!

                      Я скорее покажу как убрать из этой связки браузер. Просто возвращайте из всех компонент-листьев null. И браузер уже будет не причём. Сможете профилировать ровно то, что пишете :)

                        0
                        да, мы говорим о разных реализациях — мое упущение

                        в статье приведена наивная попытка решить проблему при помощи одного context без use-context-selection, я же обсуждаю все в контексте пакета уже с ним github.com/budarin/use-react-redux/blob/master/src/index.tsx

                        да, мы используем для отдельных разделов приложения code-splitting со своими сторами — очень много данных в каждом разделе, которые никак не пересекаются с другими разделами.

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

                          Тогда я рекомендую вам внести поправки в вашу статью, т.к. мне было предельно непонятно, что вы ведёте речь про use-context-selection. Со стороны это выглядит примерно так:


                          • redux версия такая-сякая
                          • вот моё решение, оно проще и чуть-чуть медленнее
                          • а если добавить use-context-selection, то будет также быстро

                          А на самом деле оно так:


                          • моё решение простое как валенок, но
                          • оно чертовски медленное
                          • однако если взять патченный context из use-context-selection то будет также быстро

                          К примеру даже заголовок у вас звучит так:


                          React Redux на базе Reac.Hooks + React.Context

                          Т.е. ни слова про use-context-selection, но зато чуть ниже:


                          у описанной в статье версии не значительно меньшая производительность

                          Но незначительно меньшая производительность как раз у use-context-selection версии (или местами даже лучше). А у версии в лоб она зверски медленная. И в течении всей вашей статьи у вас речь про версию в лоб (во всяком случае так статью видит читатель).


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


                          Я вот кстати не понимаю, почему, что у вас, что у redux, что… везде вот так:


                          useIsomorphicLayoutEffect(() => {
                              currentState.current = state;
                          });

                          Я вот никаких useLayoutEffect-ов не ставлю и патчу ref-ы прямо во время рендера. И я реально не вижу в чём проблема? Задача такого вот рефа всегда иметь самую актуальную версию store-а. И даже если React отбросит этот отдельно взятый render ввиду каких-то своих оптимизаций, всё равно это изменение точно не вызовет никаких багов. Зато можно точно ручаться что значение в этой переменной актуально практически всегда. Я что-то упускаю?


                          да, мы используем для отдельных разделов приложения code-splitting со своими сторами — очень много данных в каждом разделе, которые никак не пересекаются с другими разделами.

                          Flux :)

                            0
                            спасибо за продуктивное и конструктивное обсуждение!!!

                            статью писал быстро и не всё успел выверить и корректно сформулировать.
                            Оказалось написать статью — это сложнее чем написать пакет и времени забирает уйму :)

                            постараюсь исправить на досуге

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

                            по поводу рефов — сейчас точно не вспомню почему — год назад писал и по-моему там проблема в том что в текущем цикле рендера state для middlewares еще предыдущий и возникают проблемы со ссылкой на верную версию стейта в них (зацепили — придется поковырять снова :) )

                            таки Flux :) но без фанатизма!
                            у нас нет никаких связей между сторами и нет ожидания состояний сторов!

                            Истина всегда где-то посередине…
                              0
                              долго мучился вспоминал (помню были проблемы с путаницей версий состояний в контейнерах) а потом решил у Марка спросить почему он применяет useIsomorphicLayoutEffect и он ответил

                              Two reasons:

                              — Writing to refs in the middle of your render logic is a «side effect», and will especially break in React's upcoming current mode. It's only safe to mutate refs in an effect, which runs _after_ a render pass has been committed
                              — We specifically want to call `useLayoutEffect` so that the logic runs synchronously in the commit phase, not on a slight delay afterwards. However, the React team decided to print warnings every time `useLayoutEffect` is run in what _seems_ to be an SSR environment. I understand why they made that decision, but it's increidlby annoying. So, we have to dynamically call `useLayoutEffect` in the browser, but `useEffect` (which will be a no-op) on the server. `useIsomorphicLayoutEffect` is just a pointer to one or the other depending on the environment.


                              2й пункт логичен и понятен, а вот 1й — про то что установка ref в основном потоке рендера — это сайд-эффект я чо-то подзабыл :)
                                +1

                                Ну собственно он ничего и не ответил :)


                                То что это side effect и без того очевидно. А вот как это может "break" совершенно не ясно. React может отбросить отдельно взятый render — и тогда, формально, запись в ref вовремя render-а может оказаться бесполезной\лишней и т.п..


                                Но, имхо, в большинстве подобных случаев (к примеру, как у вас в коде), это не вызовет никаких негативных последствий ни при каких обстоятельствах, просто потому что, тут ref используется как class properties из class components для того чтобы ref можно было рассматривать как гарантированный источник истины. И в этом случае useLayoutEffect как ежу футболка.


                                Ну во всяком случае я так это вижу. Мы в коде используем даже такое:


                                export const useRefStorage = <T,>(value: T): T => {
                                  const ref = useRef<T>(({} as unknown) as T);
                                  Object.assign(ref.current, value);
                                  return ref.current;
                                };
                                
                                const refValue = useRefStorage(someObjectState);

                                И в итоге не нужны никакие .current и пр. штуки, и обращаться к нему можно и в render-е и в любых callback-ах (некоторые из которых могут вызываться как во время рендера, так и после, просто ввиду бизнес-логики)

                              0

                              С вашего посыла я таки прочёл эту эпичную писанину про историю развития redux. Честно говоря единственное что я для себя новое оттуда подчерпнул, так это то, что, оказывается в 6-й версии они убрали store из context-а и воткнули туда state, а потом в 7-й всё вернули как было (lol). Мои знания по большей части касались 4-й и 7-й версии. 4-ая была безнадёжно кривой, а в 7-й… ну это текущая версия. Её я дебажил не так давно.

                        0
                        про PS: не понял каким образом опциональность mapStateToProps и mapDispatchToProps поможет оптимизации? оптимизации чего?

                        Это одна из простых оптимизаций redux-а. Суть в том что mapStateToProps имеет много сигнатур из которых наибольший интерес представляют вот эти две:


                        • rootState => stateProps
                        • (rootState, ownProps) => stateProps

                        Вот у вас в коде только 2-ая. И поэтому вы добавляете props в dependencies для useMemo. А это очень сильно бьёт по производительности в ряде случаев когда эти самые state не зависят от props. И там элементарная вилка вида:


                        if (mstp.length === 2) return mstp(state, ownProps); 
                        else return mstp(state);

                        и тоже самое с зависимостями, разумеется, если мы используем useMemo.

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

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