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

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

От автора: так как новый React Context API был представлен в версии 16.3.0, у многих людей возникла мысль, достаточно ли хорош этот API, чтобы пересмотреть вопрос об использовании Redux. Мне было интересно то же самое, но я с тех пор больше не следил за данной темой, даже после выхода 16.8.0 с Hooks API. Я стремлюсь к использованию популярных технологий, не понимая всего спектра проблем, которые они решают, поэтому я слишком привык к Redux.

Ну, так получилось, что я подписался на новостную рассылку Кента С. Доддса и заметил несколько электронных писем на тему управления контекстом и состоянием, поэтому я начал читать… и читал дальше… и прочитал еще 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 он не будет повторно отображать содержимое, переданное в качестве его дочерних элементов:

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 должен получить эмиттера действия, который выбирает шутку папы, DadJoke должен получить состояние и App должен предоставить их с использованием провайдера контекста.

Мы создадим пользовательский компонент с именем DadJokeProvider, который будет внутренне управлять состоянием и помещать его дочерние элементы в провайдер контекста. Помните, что обновления его состояния не будут повторно отображать все приложение из-за дочерней оптимизации 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> )
}

Давайте также экспортируем два хука для использования значения этого контекста:

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, давайте вместо этого будем управлять состоянием с помощью редуктора, просто чтобы добавить то приятное касание излишнего уровня, которое мы знаем и любим из Redux:

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

Теперь мы можем передать этот редуктор в хук 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 // новый объект не равен новому объекту,
// это два разных объекта
console.log({} === {}) // false

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

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

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

Если мы посмотрим на нашу функцию 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 не зависит от чего-либо, что изменяется со временем, и не зависит ни от каких других эмиттеров действий, поэтому объект actions нужно создавать только один раз, при первом рендеринге:

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

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

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.

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

Автор: Matija Marohnić

Источник: https://silvenon.com

Редакция: Команда webformyself.