Встроенная альтернатива Redux с React Context и хуками

От переводчика:

Представляю вольный перевод стать о том, как реализовать эффективное решение для замены Redux контекстом React и хуками. Указание на ошибки в переводе или тексте приветствуются. Приятного просмотра.


С момента выхода нового Context API в React 16.6.0 многие люди задавали себе вопрос, достаточно ли хорош новый API, чтоб рассматривать его как замену Redux? Я думал о том же, но до конца не понимал даже после выхода версии 16.8.0 с хуками. Я стараюсь пользоваться популярными технологиями, не всегда понимая всего спектра проблем, которые они решают, так что я слишком сильно привык к Redux.

И вот так получилось, что я подписался на новостную рассылку от Кента Си Доддс (Kent C. Dodds’) и обнаружил несколько email на тему контекста и управлением состоянием. Я начал читать…. и читать… и спустя 5 блог постов что-то щелкнуло.

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

Для подготовки, давайте начнем с двух, казалось бы, случайных советов.

Во-первых, позвольте представить моего друга console.count:

console.count('Button') // Button: 1 console.count('Button') // Button: 2 console.count('App') // App: 1 console.count('Button') // Button: 3 

Мы добавим вызов console.count в каждый компонент, чтобы посмотреть сколько раз он ре-рендерится. Довольно прикольно, да?

Во-вторых, когда React компонент ре-рендерится, он не ре-рендерит контент, переданный как children.

function Parent({ children }) {   const [count, setCount] = React.useState()   console.count('Parent')   return (     <div>       <button type="button" onClick={() => {         setCount(count => count + 1)       }}>         Force re-render       </button>       {children}     </div>   ) }  function Child() {   console.count('Child')   return <div /> }  function App() {   return (     <Parent>       <Child />     </Parent>   ) }

После нескольких кликов по кнопке, вы должны увидеть следующее содержимое в консоли:

Parent: 1 Child: 1 Parent: 2 Parent: 3 Parent: 4

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

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

import React from 'react'  function Button() {   console.count('Button')   return (     <button type="button">       Fetch dad joke     </button>   ) }  function DadJoke() {   console.count('DadJoke')   return (     <p>Fetched dad joke</p>   ) }  function App() {   console.count('App')   return (     <div>       <Button />       <DadJoke />     </div>   ) }  export default App 

Button должна получить генератор действия (прим. Action Creator. Перевод взят из документации Redux на русском языке) который будет получать анекдот. DadJoke должен получить состояние, и App отобразить оба компонента используя контекст Provider.

Теперь создадим пользовательский компонент и назовем его DadJokeProvider, который внутри себя будет управлять состоянием и оборачивать дочерние компоненты в Context Provider. Помните, что обновление его состояния не будет ре-рендерить все приложение благодаря упомянутой выше оптимизации children в React.

Итак, создадим файл и назовем его contexts/dad-joke.js:

import React from 'react'  const DadJokeContext = React.createContext()  export function DadJokeContextProvider({ children }) {   const state = { dadJoke: null }   const actions = {     fetchDadJoke: () => {},   }   return (     <DadJokeContext.Provider value={{ state, actions }}>       {children}     </DadJokeContext.Provider>   ) }

Так же экспортируем 2 хука для получения значения из контекста.

export function useDadJokeState() {   return React.useContext(DadJokeContext).state }  export function useDadJokeActions() {   return React.useContext(DadJokeContext).actions }

Теперь мы уже можем реализовать это:

import React from 'react' import {   DadJokeProvider,   useDadJokeState,   useDadJokeActions, } from './contexts/dad-joke'  function Button() {   const { fetchDadJoke } = useDadJokeActions()   console.count('Button')   return (     <button type="button" onClick={fetchDadJoke}>       Fetch dad joke     </button>   ) }  function DadJoke() {   const { dadJoke } = useDadJokeState()   console.count('DadJoke')   return (     <p>{dadJoke}</p>   ) }  function App() {   console.count('App')   return (     <DadJokeProvider>       <Button />       <DadJoke />     </DadJokeProvider>   ) }  export default App

Вот! Спасибо API, который мы сделали, используя хуки. Мы больше не будем делать никаких изменений в этом файле на протяжении всего поста.

Начнем добавлять функционал в наш файл с контекстом, начиная с состояния DadJokeProvider. Да, мы могли бы просто использовать хук useState, но давайте вместо этого управлять нашим состоянием через reducer, просто добавив хорошо известный и любимый нами функционал Redux.

function reducer(state, action) {   switch (action.type) {     case 'SET_DAD_JOKE':       return {         ...state,         dadJoke: action.payload,       }     default:       return new Error();   } }

Теперь мы можем передать этот reducer в хук useReducer и получить анекдоты с API:

 export function DadJokeProvider({ children }) {   const [state, dispatch] = React.useReducer(reducer, { dadJoke: null })    async function fetchDadJoke() {     const response = await fetch('https://icanhazdadjoke.com', {       headers: {         accept: 'application/json',       },     })     const data = await response.json()     dispatch({       type: 'SET_DAD_JOKE',       payload: data.joke,     })   }    const actions = {     fetchDadJoke,   }    return (     <DadJokeContext.Provider value={{ state, actions }}>       {children}     </DadJokeContext.Provider>   ) }

Должно работать! Клик по кнопке должен получить и отображать шутки!

Давайте проверим консоль:

App: 1 Button: 1 DadJoke: 1 Button: 2 DadJoke: 2 Button: 3 DadJoke: 3

Оба компонента ре-рендерятся каждый раз, когда обновляется состояние, но только один из них реально использует его. Представьте себе реальное приложение, в котором сотни компонентов используют только действия. Было бы неплохо, если бы мы могли предоставить все эти необязательные ре-рендеры?

И тут мы вступаем на территорию относительного равенства, поэтому небольшое напоминание:

const obj = {} // ссылка равна ссылке на саму себя console.log(obj === obj) // true  // новый объект не равен другому новому объекту // Это 2 разный объекта console.log({} === {}) // false

Компонент, использующий контекст, будет ре-рендериться каждый раз, когда значение этого контекста изменяется. Давайте рассмотрим значение нашего Context Provider:

<DadJokeContext.Provider value={{ state, actions }}>

Здесь мы создаем новый объект во время каждого ре-рендера, но это неизбежно, потому что новый объект будет создаваться каждый раз, когда мы будем выполнять действие (dispatch), поэтому просто невозможно закешировать (memoize) это значение.

И все это выглядит как конец истории, да?

Если посмотрим функцию fetchDadJoke, единственное, что она использует из внешней области видимости это dispatch, правильно? В общем, я собираюсь открыть вам небольшой секрет о функциях, созданных в useReducer и useState. Для краткости я буду использовать useState в качестве примера:

 let prevSetCount  function Counter() {   const [count, setCount] = React.useState()   if (typeof prevSetCount !== 'undefined') {     console.log(setCount === prevSetCount)   }   prevSetCount = setCount   return (     <button type="button" onClick={() => {       setCount(count => count + 1)     }}>       Increment     </button>   ) }

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

true true true

Вы заметите, что setCount одна та же функция для каждого рендера. Это так же применимо и для нашей dispatch функции.

Это означает, что наша функция fetchDadJoke не зависит от чего-либо, что меняется со временем, и не зависит ни от каких других генераторов действий, поэтому объект действий нужно создавать только один раз, при первом рендере:

const actions = React.useMemo(() => ({     fetchDadJoke,   }), []) 

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

const DadJokeStateContext = React.createContext() const DadJokeActionsContext = React.createContext()

Мы можем объединить оба контекста в нашем DadJokeProvider:

 return (     <DadJokeStateContext.Provider value={state}>       <DadJokeActionsContext.Provider value={actions}>         {children}       </DadJokeActionsContext.Provider>     </DadJokeStateContext.Provider>   )

И подправить наши хуки:

export function useDadJokeState() {   return React.useContext(DadJokeStateContext) }  export function useDadJokeActions() {   return React.useContext(DadJokeActionsContext) }

И мы закончили! Серьезно, загрузите столько анекдотов, сколько хотите и убедитесь в этом сами.

App: 1 Button: 1 DadJoke: 1 DadJoke: 2 DadJoke: 3 DadJoke: 4 DadJoke: 5

Вот вы и реализовали свое собственное оптимизированное решение для управления состоянием! Вы можете создавать различные провайдеры, используя этот двухконтекстный шаблон для создания своего приложения, но и это еще не все, вы также можете рендерить один и тот же компонент провайдера несколько раз! Чтооо?! Да, попробуйте, рендер DadJokeProvider в нескольких местах и смотрите, как ваша реализация управления состоянием легко масштабируется!

Дайте волю вашему воображению и пересмотрите, зачем вам действительно нужен Redux.

Спасибо Кенту Си Доддс (Kent C. Dodds) за статьи о двухконтекстном шаблоне. Я нигде больше не видел его и мне кажется это меняет правила игры.

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

Когда использовать useMemo и useCallback
Как оптимизировать значение контекста
Как эффективно использовать React Context
Управление состояним приложения в React.
Один простой трюк для оптимизации ре-рендеров в React


ссылка на оригинал статьи https://habr.com/ru/post/463607/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *