Как эффективно управлять состоянием в React
Создание состояний по всему миру может замедлить производительность вашего приложения. Узнайте, как эффективно создавать и использовать состояния в вашем приложении React.
Если вы написали много кода React, скорее всего, вы неправильно использовали состояние. Одна из распространенных ошибок, допускаемых многими разработчиками React, — это хранение состояний в приложении глобально, а не в компонентах, в которых они используются.
Узнайте, как можно выполнить рефакторинг кода для использования локального состояния и почему это всегда хорошая идея.
Базовый пример состояния в React
Вот очень простое приложение-счетчик, которое иллюстрирует, как обычно обрабатывается состояние в React:
import {useState} from 'react'
import {Counter} from 'counter'
function App(){
const [count, setCount] = useState(0)
return <Counter count={count} setCount={setCount} />
}
export default App
В строках 1 и 2 вы импортируете хук useState() для создания состояния и компонент Counter. Вы определяете состояние count и метод setCount для обновления состояния. Затем вы передаете оба компонента Counter.
Затем компонент Counter отображает count и вызывает setCount для увеличения и уменьшения счетчика.
function Counter({count, setCount}) {
return (
<div>
<button onClick={() => setCount(prev => prev - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
</div>
)
}
Вы не определили переменную count и функцию setCount локально внутри компонента Counter. Скорее, вы передали его из родительского компонента (App). Другими словами, вы используете глобальное состояние.
Проблема с глобальными состояниями
Проблема с использованием глобального состояния заключается в том, что вы сохраняете состояние в родительском компоненте (или родителе родительского компонента), а затем передаете его в качестве реквизита тому компоненту, где это состояние действительно необходимо.
Иногда это нормально, если у вас есть состояние, которое используется многими компонентами. Но в этом случае ни один другой компонент не заботится о состоянии count, за исключением компонента Counter. Поэтому лучше переместить состояние в компонент Counter, где оно фактически используется.
Перемещение состояния в дочерний компонент
Когда вы переместите состояние в компонент Counter, оно будет выглядеть так:
import {useState} from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(prev => prev - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
</div>
)
}
Тогда внутри вашего компонента App вам не нужно ничего передавать компоненту Counter:
// imports
function App(){
return <Counter />
}
Счетчик будет работать точно так же, как и раньше, но большая разница в том, что все ваши состояния находятся локально внутри этого компонента Counter. Итак, если вам нужно иметь еще один счетчик на главной странице, вам потребуется два независимых счетчика. Каждый счетчик автономен и заботится обо всем своем состоянии.
Обработка состояния в более сложных приложениях
Другая ситуация, когда вы можете использовать глобальное состояние, — это формы. Компонент App, представленный ниже, передает данные формы (адрес электронной почты и пароль) и метод установки компоненту LoginForm.
import { useState } from "react";
import { LoginForm } from "./LoginForm";
function App() {
const [formData, setFormData] = useState({
email: "",
password: "",
});
function updateFormData(newData) {
setFormData((prev) => {
return { ...prev, ...newData };
});
}
function onSubmit() {
console.log(formData);
}
return (
<LoginForm
data={formData}
updateData={updateFormData}
onSubmit={onSubmit}
/>
);
}
Компонент LoginForm принимает информацию для входа и отображает ее. Когда вы отправляете форму, она вызывает функцию updateData, которая также передается из родительского компонента.
function LoginForm({ onSubmit, data, updateData }) {
function handleSubmit(e) {
e.preventDefault();
onSubmit();
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
value={data.email}
onChange={(e) => updateData({ email: e.target.value })}
/>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={data.password}
onChange={(e) => updateData({ password: e.target.value })}
/>
<button type="submit">Submit</button>
</form>
);
}
Вместо управления состоянием родительского компонента лучше переместить состояние в LoginForm.js, где вы будете использовать код. Это делает каждый компонент автономным и не зависит от другого компонента (т. е. родительского) для получения данных. Вот модифицированная версия LoginForm:
import { useRef } from "react";
function LoginForm({ onSubmit }) {
const emailRef = useRef();
const passwordRef = useRef();
function handleSubmit(e) {
e.preventDefault();
onSubmit({
email: emailRef.current.value,
password: passwordRef.current.value,
});
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input type="email" id="email" ref={emailRef} />
<label htmlFor="password">Password</label>
<input type="password" id="password" ref={passwordRef} />
<button type="submit">Submit</button>
</form>
);
}
Здесь вы привязываете входные данные к переменной, используя атрибуты ref и хук useRef React, вместо того, чтобы напрямую передавать методы обновления. Это поможет вам удалить подробный код и оптимизировать производительность формы с помощью перехватчика useRef.
В родительском компоненте (App.js) вы можете удалить как глобальное состояние, так и метод updateFormData(), поскольку они вам больше не нужны. Единственная оставшаяся функция — это onSubmit(), которую вы вызываете из компонента LoginForm для регистрации данных для входа в консоль.
function App() {
function onSubmit(formData) {
console.log(formData);
}
return (
<LoginForm
data={formData}
updateData={updateFormData}
onSubmit={onSubmit}
/>
);
}
Вы не только сделали свое состояние максимально локальным, но и вообще устранили необходимость в каком-либо состоянии (и вместо этого использовали refs). Таким образом, ваш компонент App стал значительно проще (имея всего одну функцию).
Ваш компонент LoginForm также стал проще, поскольку вам не нужно беспокоиться об обновлении состояния. Скорее, вы просто отслеживаете две ссылки, и все.
Обработка общего состояния
Есть одна проблема с подходом, направленным на то, чтобы сделать штат как можно более локальным. Часто приходится сталкиваться со сценариями, в которых родительский компонент не использует состояние, а передает его нескольким компонентам.
Примером может служить родительский компонент TodoContainer с двумя дочерними компонентами: TodoList и TodoCount.
function TodoContainer() {
const [todos, setTodos] = useState([])
return (
<>
<TodoList todos={todos}>
<TodoCount todos={todos}>
</>
)
}
Оба этих дочерних компонента требуют состояния todos, поэтому TodoContainer передает его им обоим. В подобных сценариях вам необходимо сделать состояние как можно более локальным. В приведенном выше примере размещение внутри TodosContainer является настолько локальным, насколько это возможно.
Если бы вы поместили это состояние в свой компонент App, оно не было бы настолько локальным, насколько это возможно, поскольку оно не является ближайшим родительским элементом для двух компонентов, которым нужны данные.
Для больших приложений управление состоянием с помощью хука useState() может оказаться затруднительным. В таких случаях вам может потребоваться выбрать React Context API или React Redux для эффективного управления состоянием.
Узнайте больше о React Hooks
Хуки составляют основу React. Используя хуки в React, вы можете избежать написания длинного кода, в котором в противном случае использовались бы классы. Хук useState(), несомненно, является наиболее часто используемым хуком React, но существует множество других, таких как useEffect(), useRef() и useContext().
Если вы хотите овладеть навыками разработки приложений с помощью React, вам необходимо знать, как использовать эти хуки в своем приложении.