Представляю вольный перевод стать о том, как реализовать эффективное решение для замены 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/
Добавить комментарий