Хуки состояния в Mantine: три инструмента
Формы и списки — это места в приложении, где обычный useState быстро обрастает лишним кодом: то нужно доставать значение из события, то вручную копировать массив, то собирать объект по кусочкам.
Библиотека Mantine вынесла эти повторяющиеся сценарии в отдельные хуки — useInputState, useListState и useSetState. В этом уроке разберём, что умеет каждый из них и в каких ситуациях они экономят реальное время разработки.
Зачем нужны хуки состояния в Mantine
useState отлично справляется с примитивными значениями, но стоит задаче усложниться — и вокруг него начинает расти шаблонный код.Для контролируемого инпута приходится писать отдельную функцию-обработчик, которая достаёт event.target.value. Для списка нужно каждый раз вручную создавать новый массив через spread, filter или map, чтобы не мутировать состояние напрямую. А для объекта с несколькими полями — аккуратно собирать новый объект, не забыв ни одного свойства.
Три хука из @mantine/hooks, о которых пойдёт речь в этом уроке, закрывают именно эти три сценария.
useInputState— управление одним значением инпута без ручного обработчикаonChangeuseListState— иммутабельные операции над массивом: добавление, удаление, сортировка, изменение элементовuseSetState— частичное обновление объекта состояния, какthis.setStateв классовых компонентах
Ниже разберём каждый хук отдельно на практическом примере, а в конце урока соберём их в одну сравнительную таблицу, чтобы было проще выбрать нужный инструмент под конкретную задачу.
useInputState — контролируемые поля без обработчиков
useInputState работает как обычный useState: возвращает кортеж из значения и функции для его изменения.Разница в том, что сеттер можно передавать прямо в проп onChange компонента. Хук сам определит, пришло ли ему готовое значение или событие, и во втором случае автоматически достанет event.target.value.
import { useInputState } from "@mantine/hooks";
import { TextInput } from "@mantine/core";
function NameField() {
const [value, setValue] = useInputState("");
return (
<TextInput
label="Имя"
value={value}
onChange={setValue}
/>
);
}
Дженерик у useInputState<T> позволяет явно указать тип значения — например, useInputState<number>(0) для числового поля. Это особенно удобно в TypeScript-проектах: компилятор сразу подскажет, если куда-то случайно передать строку вместо числа.
Чекбоксы и переключатели тоже под контролем
Хук одинаково хорошо работает и с чекбоксами. Когда компонент вроде Checkbox вызывает onChange, useInputState проверяет тип элемента и в случае чекбокса или переключателя достаёт значение из event.currentTarget.checked, а не из value. Разработчику не нужно помнить об этой разнице — хук берёт её на себя.
import { useInputState } from "@mantine/hooks";
import { Checkbox } from "@mantine/core";
function AgreeCheckbox() {
const [checked, setChecked] = useInputState(false);
return (
<Checkbox
label="Согласен с условиями"
checked={checked}
onChange={setChecked}
/>
);
}
useListState — иммутабельные списки без ручной возни
Как только состояние превращается в массив, количество шаблонного кода растёт: добавить элемент — значит создать новый массив с помощью spread-оператора, удалить — вызвать filter, изменить один элемент — пройтись map-ом по всему массиву. Хук useListState<T>(initialValue) прячет всю эту логику за набором готовых методов.
Хук возвращает не пару, а кортеж из значений массива и объекта handlers с методами: append, prepend, insert, remove, pop, shift, reorder, swap, applyWhere, apply, setItem, setItemProp, filter и setState. Каждый метод возвращает новый массив, не трогая исходный, — про иммутабельность можно вообще не думать.
import { useListState } from "@mantine/hooks";
import { Button, Checkbox, Group } from "@mantine/core";
interface Task {
id: string;
title: string;
done: boolean;
}
function TaskList() {
const [tasks, handlers] = useListState<Task>([
{ id: "1", title: "Изучить Mantine", done: false },
]);
const addTask = () =>
handlers.append({
id: Date.now().toString(),
title: "Новая задача",
done: false,
});
const toggleTask = (id: string) =>
handlers.applyWhere(
(task) => task.id === id,
(task) => ({ ...task, done: !task.done })
);
return (
<div>
{tasks.map((task) => (
<Group key={task.id}>
<Checkbox
checked={task.done}
onChange={() => toggleTask(task.id)}
label={task.title}
/>
</Group>
))}
<Button onClick={addTask}>Добавить задачу</Button>
</div>
);
}
При рендере элементов, полученных из useListState, используйте в качестве key стабильный уникальный id самого элемента, а не его индекс в массиве. Если список умеет пересортировываться или из середины удаляются элементы, key по индексу заставит React путать элементы местами и лишний раз перемонтировать DOM-узлы вместо того, чтобы просто переставить их.
useSetState — частичное обновление объекта одной функцией
Если состояние — это не примитив и не массив, а объект с несколькими полями, на помощь приходит useSetState<T>(initialState). Он воспроизводит поведение this.setState из классовых компонентов: сеттер принимает не весь новый объект, а только изменившиеся поля, и сам аккуратно сливает их с текущим состоянием.
Сеттер также умеет принимать функцию-обновление, которая получает текущее состояние и возвращает частичный объект для слияния — это полезно, когда новое значение поля зависит от предыдущего состояния.
import { useSetState } from "@mantine/hooks";
import { NumberInput, TextInput } from "@mantine/core";
interface ProfileState {
name: string;
age: number;
city: string;
}
function ProfileForm() {
const [state, setState] = useSetState<ProfileState>({
name: "",
age: 0,
city: "",
});
return (
<div>
<TextInput
label="Имя"
value={state.name}
onChange={(e) => setState({ name: e.currentTarget.value })}
/>
<NumberInput
label="Возраст"
value={state.age}
onChange={(age) => setState((current) => ({ age: Number(age) || 0 }))}
/>
</div>
);
}
Собери свой код. Запусти сайт!
От наброска на салфетке до первого работающего лендинга. Наш онлайн-курс «Веб-верстка с нуля и до профессионала» — это интенсивный трек, где ты не будешь зубрить теорию, а с первого дня начнешь превращать идеи в чистый HTML и CSS.
Собери свой первый проект под руководством практикующих разработчиков.
Сравниваем три хука
Если меняется одно-единственное поле формы, не стоит тянуть ради него useSetState или useListState — достаточно обычного useInputState. И наоборот, для динамического списка из десятков элементов ручной useState со spread-оператором быстро станет источником трудноуловимых багов, поэтому здесь оправдан именно useListState.
| Хук | Что хранит | Когда использовать |
|---|---|---|
useInputState |
Одно значение — строку, число или boolean |
Контролируемый инпут без ручного onChange |
useListState |
Массив объектов или примитивов | Списки задач, таблицы, drag-and-drop, todo-приложения |
useSetState |
Объект с несколькими полями | Формы с несколькими полями, замена this.setState |
Если список, отрисованный через useListState, может разрастись до сотен элементов, не создавайте инлайн-функции вроде onClick={() => toggleTask(task.id)} прямо в JSX каждого элемента. Вынесите строку списка в отдельный компонент, оберните его в React.memo и передавайте только id — тогда React не будет пересоздавать замыкания и перерисовывать все элементы при каждом изменении состояния.
Итоги
Мы разобрали три хука Mantine для работы с состоянием: useInputState избавляет от ручных обработчиков для одиночных полей, useListState берёт на себя иммутабельные операции над массивами, а useSetState позволяет обновлять объект частями, как в классовых компонентах. Все три хука живут в одном и том же пакете @mantine/hooks, не требуют установки дополнительных зависимостей и свободно комбинируются в одном компоненте — например, можно использовать useListState для списка задач и useSetState для формы добавления новой задачи.
В следующем уроке разберём хуки useLocalStorage и useSessionStorage — с их помощью состояние, которое мы сегодня научились аккуратно обновлять, можно будет сохранять прямо в браузере и восстанавливать после перезагрузки страницы.