Введение
Самым сложным аспектом разработки любого приложения часто является управление его состоянием. Однако нам часто приходится управлять несколькими частями состояния в нашем приложении, например, когда данные извлекаются с внешнего сервера или обновляются в приложении.
Сложность управления состоянием является причиной того, что сегодня существует так много библиотек управления состоянием — и еще больше библиотек продолжают разрабатываться. К счастью, в React есть несколько встроенных решений в виде хуков для управления состояниями, которые упрощают управление состояниями в React.
Не секрет, что хуки стали играть все более важную роль в разработке компонентов React, особенно функциональных компонентов, поскольку они полностью заменили необходимость в компонентах на основе классов, которые были традиционным способом управления компонентами с состояниями. Хук useState
является одним из многих хуков, представленных в React, но хотя хук useState
существует уже несколько лет, разработчики все еще склонны совершать распространенные ошибки из-за недостаточного понимания.
Хук useState
может быть сложным для понимания, особенно для начинающих разработчиков React или тех, кто переходит от компонентов на основе классов к функциональным компонентам. В этом руководстве мы рассмотрим 5 самых распространенных ошибок useState
, которые часто допускают разработчики React, и как их можно избежать.
Шаги, которые мы рассмотрим:
- Неправильная инициализация useState
- Неиспользование опциональной цепочки
- Обновление useState напрямую
- Обновление конкретного свойства объекта
- Управление несколькими полями ввода в формах
Неправильная инициализация useState
Неправильная инициализация хука useState — одна из самых распространенных ошибок, которые допускают разработчики при его использовании. Что значит инициализировать useState? Инициализировать что-то — значит установить его начальное значение.
Проблема в том, что useState позволяет определить начальное состояние, используя все, что угодно. Однако никто не говорит вам прямо, что в зависимости от того, что ожидает ваш компонент в этом состоянии, использование неправильного значения типа даты для инициализации useState может привести к неожиданному поведению вашего приложения, например, к невозможности отрисовки пользовательского интерфейса, что приведет к ошибке пустого экрана.
Например, у нас есть компонент, ожидающий состояние объекта пользователя, содержащее его имя, изображение и биографию — и в этом компоненте мы отрисовываем свойства пользователя.
Инициализация useState другим типом данных, например, пустым состоянием или нулевым значением, приведет к ошибке пустой страницы, как показано ниже.
import { useState } from "react";
function App() {
// Initializing state
const [user, setUser] = useState();
// Render UI
return (
<div className='App'>
<img src={user.image} alt='profile image' />
<p>User: {user.name}</p>
<p>About: {user.bio}</p>
</div>
);
}
export default App;
Выход:
При просмотре консоли возникнет аналогичная ошибка, как показано ниже:
Начинающие разработчики часто допускают эту ошибку при инициализации состояния, особенно при получении данных с сервера или базы данных, поскольку ожидается, что полученные данные обновят состояние с реальным объектом пользователя. Однако это плохая практика и может привести к ожидаемому поведению, как показано выше.
Предпочтительным способом инициализации useState
является передача ему ожидаемого типа данных, чтобы избежать потенциальных ошибок пустой страницы. Например, можно передать состоянию пустой объект, как показано ниже:
import { useState } from "react";
function App() {
// Initializing state with expected data type
const [user, setUser] = useState({});
// Render UI
return (
<div className='App'>
<img src={user.image} alt='profile image' />
<p>User: {user.name}</p>
<p>About: {user.bio}</p>
</div>
);
}
export default App;
Выход:
Мы можем пойти еще дальше, определив ожидаемые свойства пользовательского объекта при инициализации состояния.
import { useState } from "react";
function App() {
// Initializing state with expected data type
const [user, setUser] = useState({
image: "",
name: "",
bio: "",
});
// Render UI
return (
<div className='App'>
<img src={user.image} alt='profile image' />
<p>User: {user.name}</p>
<p>About: {user.bio}</p>
</div>
);
}
export default App;
Неиспользование необязательной цепочки
Иногда просто инициализировать useState ожидаемым типом данных бывает недостаточно для предотвращения неожиданной ошибки пустой страницы. Это особенно верно при попытке получить доступ к свойству глубоко вложенного объекта, находящегося внутри цепочки связанных объектов.
Обычно вы пытаетесь получить доступ к этому объекту, перебирая связанные объекты с помощью оператора цепочки с точкой (.), например, user.names.firstName
. Однако у нас возникает проблема, если какие-либо связанные объекты или свойства отсутствуют. Страница оборвется, и пользователь получит ошибку пустой страницы.
Например:
import { useState } from "react";
function App() {
// Initializing state with expected data type
const [user, setUser] = useState({});
// Render UI
return (
<div className='App'>
<img src={user.image} alt='profile image' />
<p>User: {user.names.firstName}</p>
<p>About: {user.bio}</p>
</div>
);
}
export default App;
Ошибка вывода:
Типичным решением этой ошибки и отсутствия рендеринга пользовательского интерфейса является использование условных проверок для подтверждения существования состояния, чтобы проверить, доступно ли оно перед рендерингом компонента, например, user.names && user.names.firstName
, который оценивает правое выражение только в том случае, если левое выражение истинно (если user.names
существует). Однако такое решение является муторным, поскольку потребует нескольких проверок для каждой цепочки объектов.
Используя дополнительный оператор цепочки (?.)
, вы можете прочитать значение свойства, находящегося глубоко внутри связанной цепочки объектов, без необходимости проверять, что каждый ссылающийся объект действителен. Дополнительный оператор цепочки (?.)
подобен оператору цепочки точек (.)
, за исключением того, что если ссылающийся объект или свойство отсутствует (т.е. null или undefined), выражение замыкается и возвращает значение undefined. Проще говоря, если какой-либо объект цепочки отсутствует, то цепочка не продолжается (замыкается).
Например, user?.names?.firstName
не выдаст никакой ошибки и не сломает страницу, потому что, обнаружив отсутствие объекта user или names, он немедленно завершает операцию.
import { useState } from "react";
function App() {
// Initializing state with expected data type
const [user, setUser] = useState({});
// Render UI
return (
<div className='App'>
<img src={user.image} alt='profile image' />
<p>User: {user?.names?.firstName}</p>
<p>About: {user.bio}</p>
</div>
);
}
export default App;
Использование опционального оператора цепочки может упростить и сократить выражения при обращении к цепочке свойств в состоянии, что может быть очень полезно при исследовании содержимого объектов, ссылка на которые может быть заранее неизвестна.
Обновление useState напрямую
Отсутствие правильного понимания того, как React планирует и обновляет состояние, может легко привести к ошибкам при обновлении состояния приложения. При использовании useState
мы обычно определяем состояние и напрямую обновляем его с помощью функции set state.
Например, мы создаем состояние count и функцию-обработчик, прикрепленную к кнопке, которая добавляет единицу (+1) к состоянию при нажатии:
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
// Directly update state
const increase = () => setCount(count + 1);
// Render UI
return (
<div className='App'>
<span>Count: {count}</span>
<button onClick={increase}>Add +1</button>
</div>
);
}
export default App;
Результат:
Все работает, как и ожидалось. Однако прямое обновление состояния — это плохая практика, которая может привести к потенциальным ошибкам при работе с живым приложением, которое используют несколько пользователей. Почему? Потому что, вопреки вашему мнению, React не обновляет состояние сразу после нажатия на кнопку, как показано в демонстрационном примере. Вместо этого React делает снимок текущего состояния и планирует это обновление (+1) на более позднее время для повышения производительности — это происходит за миллисекунды, поэтому незаметно для человеческого глаза. Однако, пока запланированное обновление находится в ожидании перехода, текущее состояние может быть изменено чем-то другим (например, делами нескольких пользователей). Запланированное обновление никак не сможет узнать об этом новом событии, потому что у него есть только запись снимка состояния, который оно сделало, когда была нажата кнопка.
Это может привести к серьезным ошибкам и странному поведению вашего приложения. Давайте посмотрим это в действии, добавив еще одну кнопку, которая асинхронно обновляет состояние счетчика с задержкой в 2 секунды.
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
// Directly update state
const update = () => setCount(count + 1);
// Directly update state after 2 sec
const asyncUpdate = () => {
setTimeout(() => {
setCount(count + 1);
}, 2000);
};
// Render UI
return (
<div className='App'>
<span>Count: {count}</span>
<button onClick={update}>Add +1</button>
<button onClick={asyncUpdate}>Add +1 later</button>
</div>
);
}
Обратите внимание на ошибку в выводе:
Заметили ошибку? Мы начинаем с того, что дважды нажимаем на первую кнопку «Добавить +1» (которая обновляет состояние до 1 + 1 = 2). После этого мы нажимаем на кнопку «Добавить +1 позже» — это делает снимок текущего состояния (2) и планирует обновление для добавления 1 к этому состоянию через две секунды. Но пока это запланированное обновление находится на стадии перехода, мы трижды нажимаем на кнопку «Добавить +1», обновляя текущее состояние до 5 (т.е. 2 + 1 + 1 + 1 = 5). Однако асинхронный запланированный Update пытается обновить состояние через две секунды, используя снимок (2), хранящийся в памяти (т.е. 2 + 1 = 3), не понимая, что текущее состояние было обновлено до 5. В результате состояние обновляется до 3 вместо 6.
Эта непреднамеренная ошибка часто возникает в приложениях, состояния которых обновляются непосредственно с помощью функции setState(newValue). Предлагаемый способ обновления useState — это функциональное обновление, при котором setState()
передается функции обратного вызова, а в эту функцию обратного вызова мы передаем текущее состояние в данный момент, например, setState(currentState => currentState + newValue)
. Это передает текущее состояние в запланированное время обновления в функцию обратного вызова, что позволяет узнать текущее состояние перед попыткой обновления.
Итак, давайте изменим демонстрационный пример, чтобы использовать функциональное обновление вместо прямого обновления.
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
// Directly update state
const update = () => setCount(count + 1);
// Directly update state after 3 sec
const asyncUpdate = () => {
setTimeout(() => {
setCount((currentCount) => currentCount + 1);
}, 2000);
};
// Render UI
return (
<div className='App'>
<span>Count: {count}</span>
<button onClick={update}>Add +1</button>
<button onClick={asyncUpdate}>Add +1 later</button>
</div>
);
}
export default App;
Выход:
При функциональном обновлении функция setState()
знает, что состояние было обновлено до 5, поэтому она обновляет состояние до 6.
Обновление конкретного свойства объекта
Еще одна распространенная ошибка — изменение только свойства объекта или массива, а не самой ссылки.
Например, мы инициализируем объект пользователя с определенными свойствами «имя» и «возраст». Однако в нашем компоненте есть кнопка, которая пытается обновить только имя пользователя, как показано ниже.
import { useState, useEffect } from "react";
export default function App() {
const [user, setUser] = useState({
name: "John",
age: 25,
});
// Update property of user state
const changeName = () => {
setUser((user) => (user.name = "Mark"));
};
// Render UI
return (
<div className='App'>
<p>User: {user.name}</p>
<p>Age: {user.age}</p>
<button onClick={changeName}>Change name</button>
</div>
);
}
Исходное состояние до нажатия кнопки:
Обновленное состояние после нажатия кнопки:
Как вы можете видеть, вместо того, чтобы изменить конкретное свойство, пользователь больше не является объектом, а был перезаписан в строку «Mark». Почему? Потому что setState() назначает в качестве нового состояния любое возвращенное или переданное ей значение. Эта ошибка часто встречается у разработчиков React, переходящих от компонентов на основе классов к функциональным компонентам, поскольку они привыкли обновлять состояние с помощью this.state.user.property = newValue
в компонентах на основе классов.
Одним из типичных старых способов сделать это является создание новой ссылки на объект и присвоение ей предыдущего объекта user, с непосредственным изменением имени пользователя.
import { useState, useEffect } from "react";
export default function App() {
const [user, setUser] = useState({
name: "John",
age: 25,
});
// Update property of user state
const changeName = () => {
setUser((user) => Object.assign({}, user, { name: "Mark" }));
};
// Render UI
return (
<div className='App'>
<p>User: {user.name}</p>
<p>Age: {user.age}</p>
<button onClick={changeName}>Change name</button>
</div>
);
}
Обновленное состояние после нажатия кнопки:
Обратите внимание, что изменено только имя пользователя, а остальные свойства остались прежними.
Однако идеальным и современным способом обновления определенного свойства, объекта или массива является использование оператора разворота ES6 (…). Это идеальный способ обновления конкретного свойства объекта или массива при работе с состоянием в функциональных компонентах. С помощью оператора spread вы можете легко распаковать свойства существующего элемента в новый элемент и одновременно изменить или добавить новые свойства в распакованный элемент.
import { useState, useEffect } from "react";
export default function App() {
const [user, setUser] = useState({
name: "John",
age: 25,
});
// Update property of user state using spread operator
const changeName = () => {
setUser((user) => ({ ...user, name: "Mark" }));
};
// Render UI
return (
<div className='App'>
<p>User: {user.name}</p>
<p>Age: {user.age}</p>
<button onClick={changeName}>Change name</button>
</div>
);
}
Результат будет таким же, как и в предыдущем состоянии. После нажатия на кнопку обновляется свойство name, а остальные пользовательские свойства остаются неизменными.
Управление несколькими полями ввода в формах
Управление несколькими управляемыми вводами в форме обычно осуществляется путем ручного создания нескольких функций useState()
для каждого поля ввода и привязки каждой к соответствующему полю ввода. Например:
import { useState, useEffect } from "react";
export default function App() {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [age, setAge] = useState("");
const [userName, setUserName] = useState("");
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
// Render UI
return (
<div className='App'>
<form>
<input type='text' placeholder='First Name' />
<input type='text' placeholder='Last Name' />
<input type='number' placeholder='Age' />
<input type='text' placeholder='Username' />
<input type='password' placeholder='Password' />
<input type='email' placeholder='email' />
</form>
</div>
);
}
Кроме того, необходимо создать функцию-обработчик для каждого из этих входов, чтобы создать двунаправленный поток данных, который обновляет каждое состояние при вводе значения. Это может быть довольно избыточным и отнимать много времени, поскольку требует написания большого количества кода, что снижает читабельность вашей кодовой базы.
Однако можно управлять несколькими полями ввода в форме, используя только один хук useState. Этого можно достичь, если сначала дать каждому полю ввода уникальное имя, а затем создать одну функцию useState()
, которая инициализируется свойствами, идентичными именам полей ввода.
import { useState, useEffect } from "react";
export default function App() {
const [user, setUser] = useState({
firstName: "",
lastName: "",
age: "",
username: "",
password: "",
email: "",
});
// Render UI
return (
<div className='App'>
<form>
<input type='text' name='firstName' placeholder='First Name' />
<input type='text' name='lastName' placeholder='Last Name' />
<input type='number' name='age' placeholder='Age' />
<input type='text' name='username' placeholder='Username' />
<input type='password' name='password' placeholder='Password' />
<input type='email' name='email' placeholder='email' />
</form>
</div>
);
}
После этого мы создаем функцию обработчика событий, которая обновляет определенное свойство объекта user, чтобы отразить изменения в форме, когда пользователь что-то вводит. Это можно сделать с помощью оператора spread и динамического доступа к имени конкретного элемента ввода, который вызвал функцию-обработчик, используя event.target.elementsName = event.target.value
.
Другими словами, мы проверяем объект события, который обычно передается в функцию события, на наличие имени целевого элемента (которое совпадает с именем свойства в состоянии пользователя) и обновляем его с соответствующим значением в этом целевом элементе, как показано ниже:
import { useState, useEffect } from "react";
export default function App() {
const [user, setUser] = useState({
firstName: "",
lastName: "",
age: "",
username: "",
password: "",
email: "",
});
// Update specific input field
const handleChange = (e) =>
setUser(prevState => ({...prevState, [e.target.name]: e.target.value}))
// Render UI
return (
<div className='App'>
<form>
<input type='text' onChange={handleChange} name='firstName' placeholder='First Name' />
<input type='text' onChange={handleChange} name='lastName' placeholder='Last Name' />
<input type='number' onChange={handleChange} name='age' placeholder='Age' />
<input type='text' onChange={handleChange} name='username' placeholder='Username' />
<input type='password' onChange={handleChange} name='password' placeholder='Password' />
<input type='email' onChange={handleChange} name='email' placeholder='email' />
</form>
</div>
);
}
При такой реализации функция обработчика событий запускается для каждого пользовательского ввода. В этой функции события у нас есть функция состояния setUser()
, которая принимает предыдущее/текущее состояние пользователя и распаковывает это состояние пользователя с помощью оператора spread. Затем мы проверяем объект события на наличие имени целевого элемента, вызвавшего функцию (которое соотносится с именем свойства в состоянии). Получив это имя свойства, мы изменяем его, чтобы отразить введенное пользователем значение в форме.
Заключение
Будучи разработчиком React, создающим высокоинтерактивные пользовательские интерфейсы, вы наверняка совершали некоторые из вышеперечисленных ошибок. Надеемся, что эти полезные методы использования UseState помогут вам избежать некоторых из этих потенциальных ошибок при использовании хука useState
в дальнейшем при создании ваших приложений на базе React.
Автор: Дэвид Херберт
Создавайте CRUD-приложения на основе React без ограничений
Создание CRUD-приложений включает в себя множество повторяющихся задач, отнимающих ваше драгоценное время разработки. Если вы начинаете с нуля, вам также придется внедрять собственные решения для критически важных частей приложения, таких как аутентификация, авторизация, управление состоянием и сетевое взаимодействие.
Ознакомьтесь с refine, если вы заинтересованы в безголовом фреймворке с надежной архитектурой и полным набором лучших отраслевых практик для вашего следующего CRUD-проекта.
refine — это фреймворк на базе React с открытым исходным кодом для создания CRUD-приложений без ограничений.
Он может ускорить время разработки до 3 раз без ущерба для свободы стиля, кастомизации и рабочего процесса проекта.
refine является безголовым по своей конструкции и подключает 30+ бэкенд-сервисов «из коробки», включая пользовательские REST и GraphQL API.
Посетите репозиторий refine на GitHub для получения дополнительной информации, демонстрационных версий, учебников и примеров проектов.