Подводные камни чрезмерного использования контекста React
По большей части, React и State идут рука об руку. По мере роста вашего приложения React управление состоянием становится все более и более важным.
С React 16.8 и введением хуков React Context API заметно улучшился. Теперь мы можем комбинировать это с хуками для имитации react-redux
; некоторые люди даже используют его для управления всем состоянием приложения. Однако у React Context есть некоторые подводные камни, и чрезмерное использование может привести к проблемам с производительностью.
В этом руководстве мы рассмотрим потенциальные последствия чрезмерного использования React Context и обсудим, как его эффективно использовать в вашем следующем проекте React.
React Context предоставляет способ обмениваться данными (состоянием) в вашем приложении, не передавая реквизиты на каждый компонент. Это позволяет вам использовать данные, хранящиеся в контексте, через поставщиков и потребителей без дополнительной подготовки.
const CounterContext = React.createContext();
const CounterProvider = ({ children }) => {
const [count, setCount] = React.useState(0);
const increment = () => setCount(counter => counter + 1);
const decrement = () => setCount(counter => counter - 1);
return (
<CounterContext.Provider value={{ count, increment, decrement }}>
{children}
</CounterContext.Provider>
);
};
const IncrementCounter = () => {
const { increment } = React.useContext(CounterContext);
return <button onClick={increment}> Increment</button>;
};
const DecrementCounter = () => {
const { decrement } = React.useContext(CounterContext);
return <button onClick={decrement}> Decrement</button>;
};
const ShowResult = () => {
const { count } = React.useContext(CounterContext);
return <h1>{count}</h1>;
};
const App = () => (
<CounterProvider>
<ShowResult />
<IncrementCounter />
<DecrementCounter />
</CounterProvider>
);
Обратите внимание, что я намеренно разделил IncrementCounter
и DecrementCounter
на две составляющие. Это поможет мне более четко продемонстрировать проблемы, связанные с React Context.
Как видите, у нас очень простой контекст. Он содержит две функции, increment
и decrement
, которые обрабатывают расчет и результат счетчика. Затем мы извлекаем данные из каждого компонента и отображаем их в компоненте App
. Ничего особенного, только ваше типичное приложение React.
С этой точки зрения вам может быть интересно, в чем проблема с использованием React Context? Для такого простого приложения управлять состоянием легко. Однако по мере усложнения вашего приложения React Context может быстро превратиться в кошмар разработчика.
Плюсы и минусы использования React Context
Хотя React Context прост в реализации и отлично подходит для определенных типов приложений, он построен таким образом, что каждый раз, когда изменяется значение контекста, потребитель компонента перерисовывается.
До сих пор это не было проблемой для нашего приложения, потому что, если компонент не перерисовывается при изменении значения контекста, он никогда не получит обновленное значение. Однако повторное отображение не будет ограничено потребителем компонента; все компоненты, связанные с контекстом, будут перерисованы.
Чтобы увидеть это в действии, давайте обновим наш пример.
const CounterContext = React.createContext();
const CounterProvider = ({ children }) => {
const [count, setCount] = React.useState(0);
const [hello, setHello] = React.useState("Hello world");
const increment = () => setCount(counter => counter + 1);
const decrement = () => setCount(counter => counter - 1);
const value = {
count,
increment,
decrement,
hello
};
return (
<CounterContext.Provider value={value}>{children}</CounterContext.Provider>
);
};
const SayHello = () => {
const { hello } = React.useContext(CounterContext);
console.log("[SayHello] is running");
return <h1>{hello}</h1>;
};
const IncrementCounter = () => {
const { increment } = React.useContext(CounterContext);
console.log("[IncrementCounter] is running");
return <button onClick={increment}> Increment</button>;
};
const DecrementCounter = () => {
console.log("[DecrementCounter] is running");
const { decrement } = React.useContext(CounterContext);
return <button onClick={decrement}> Decrement</button>;
};
const ShowResult = () => {
console.log("[ShowResult] is running");
const { count } = React.useContext(CounterContext);
return <h1>{count}</h1>;
};
const App = () => (
<CounterProvider>
<SayHello />
<ShowResult />
<IncrementCounter />
<DecrementCounter />
</CounterProvider>
);
Я добавил новый компонент SayHello
, который отображает сообщение из контекста. Мы также будем регистрировать сообщение всякий раз, когда эти компоненты визуализируются или повторно передаются. Таким образом, мы можем увидеть, влияет ли изменение на все компоненты.
// Result of the console
[SayHello] is running
[ShowResult] is running
[IncrementCounter] is running
[DecrementCounter] is running
Когда страница завершит загрузку, все сообщения появятся на консоли. До сих пор не о чем беспокоиться.
Давайте нажмем на кнопку increment
, чтобы увидеть, что происходит.
// Result of the console
[SayHello] is running
[ShowResult] is running
[IncrementCounter] is running
[DecrementCounter] is running
Как видите, все компоненты перерисовываются. Нажатие на кнопку decrement
имеет тот же эффект. Каждый раз, когда значение контекста изменяется, потребители всех компонентов будут повторно отображаться.
Возможно, вам все еще интересно, кого это волнует? Разве не так работает React Context?
Для такого крошечного приложения нам не нужно беспокоиться о негативных последствиях использования React Context. Но в крупном проекте с частыми изменениями состояния инструмент создает больше проблем, чем помогает решить. Простое изменение вызовет бесчисленное количество повторных обращений, что в конечном итоге приведет к значительным проблемам с производительностью.
Так как же мы можем избежать повторного рендеринга, снижающего производительность?
Предотвратить рендеринг с useMemo()
Может быть, запоминание является решением нашей проблемы. Давайте обновим наш код useMemo
, чтобы увидеть, поможет ли запоминание нашего значения избежать повторного рендеринга.
const CounterContext = React.createContext();
const CounterProvider = ({ children }) => {
const [count, setCount] = React.useState(0);
const [hello, sayHello] = React.useState("Hello world");
const increment = () => setCount(counter => counter + 1);
const decrement = () => setCount(counter => counter - 1);
const value = React.useMemo(
() => ({
count,
increment,
decrement,
hello
}),
[count, hello]
);
return (
<CounterContext.Provider value={value}>{children}</CounterContext.Provider>
);
};
const SayHello = () => {
const { hello } = React.useContext(CounterContext);
console.log("[SayHello] is running");
return <h1>{hello}</h1>;
};
const IncrementCounter = () => {
const { increment } = React.useContext(CounterContext);
console.log("[IncrementCounter] is running");
return <button onClick={increment}> Increment</button>;
};
const DecrementCounter = () => {
console.log("[DecrementCounter] is running");
const { decrement } = React.useContext(CounterContext);
return <button onClick={decrement}> Decrement</button>;
};
const ShowResult = () => {
console.log("[ShowResult] is running");
const { count } = React.useContext(CounterContext);
return <h1>{count}</h1>;
};
const App = () => (
<CounterProvider>
<SayHello />
<ShowResult />
<IncrementCounter />
<DecrementCounter />
</CounterProvider>
);
Теперь давайте снова нажмем кнопку increment
, чтобы увидеть, работает ли она.
<// Result of the console
[SayHello] is running
[ShowResult] is running
[IncrementCounter] is running
[DecrementCounter] is running
К сожалению, мы все еще сталкиваемся с той же проблемой. Потребители всех компонентов обновляются всякий раз, когда изменяется значение нашего контекста.
Если запоминание не решает проблему, должны ли мы вообще перестать управлять нашим состоянием с помощью React Context?
Если вы используете React Context?
Прежде чем начать свой проект, вы должны определить, как вы хотите управлять своим состоянием. Есть множество доступных решений, только одно из которых - React Context. Чтобы определить, какой инструмент лучше всего подходит для вашего приложения, задайте себе два вопроса:
- Когда вы должны его использовать?
- Как вы планируете использовать его?
Если ваше состояние часто обновляется, React Context может быть не таким эффективным или действенным, как такой инструмент, как React Redux. Но если у вас есть статические данные, которые подвергаются низкочастотным обновлениям, таким как предпочтительный язык, изменения времени, изменения местоположения и аутентификация пользователя, наилучшим вариантом может стать передача реквизитов с помощью React Context.
Если вы решите использовать React Context, попробуйте разделить ваш большой контекст на несколько контекстов, насколько это возможно, и держите свое состояние близко к потребителю компонента. Это поможет вам максимально расширить возможности React Context, который может быть довольно мощным в определенных сценариях для простых приложений.
Последние мысли
React Context - это отличный API для простых приложений с редкими изменениями состояния, но он может быстро превратиться в кошмар разработчика, если вы чрезмерно используете его для более сложных проектов. Знание того, как работает инструмент при создании высокопроизводительных приложений, поможет вам определить, может ли оно быть полезным для управления состояниями в вашем проекте. Несмотря на свои ограничения при работе с высокой частотой изменений состояния, React Context является очень мощным решением для управления состоянием при правильном использовании.