Хотите заказать веб-сайт? Связаться с нами

Хуки состояния в Mantine: три инструмента

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

Библиотека Mantine вынесла эти повторяющиеся сценарии в отдельные хуки — useInputState, useListState и useSetState. В этом уроке разберём, что умеет каждый из них и в каких ситуациях они экономят реальное время разработки.

Хуки состояния в Mantine: три инструмента

Зачем нужны хуки состояния в Mantine

Обычный useState отлично справляется с примитивными значениями, но стоит задаче усложниться — и вокруг него начинает расти шаблонный код.

Для контролируемого инпута приходится писать отдельную функцию-обработчик, которая достаёт event.target.value. Для списка нужно каждый раз вручную создавать новый массив через spread, filter или map, чтобы не мутировать состояние напрямую. А для объекта с несколькими полями — аккуратно собирать новый объект, не забыв ни одного свойства.

Три хука из @mantine/hooks, о которых пойдёт речь в этом уроке, закрывают именно эти три сценария.

  1. useInputState — управление одним значением инпута без ручного обработчика onChange
  2. useListState — иммутабельные операции над массивом: добавление, удаление, сортировка, изменение элементов
  3. 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 (
    &lt;Checkbox
      label="Согласен с условиями"
      checked={checked}
      onChange={setChecked}
    /&gt;
  );
}

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&lt;Task&gt;([
    { id: "1", title: "Изучить Mantine", done: false },
  ]);
 
  const addTask = () =&gt;
    handlers.append({
      id: Date.now().toString(),
      title: "Новая задача",
      done: false,
    });
 
  const toggleTask = (id: string) =&gt;
    handlers.applyWhere(
      (task) =&gt; task.id === id,
      (task) =&gt; ({ ...task, done: !task.done })
    );
 
  return (
    &lt;div&gt;
      {tasks.map((task) =&gt; (
        &lt;Group key={task.id}&gt;
          &lt;Checkbox
            checked={task.done}
            onChange={() =&gt; toggleTask(task.id)}
            label={task.title}
          /&gt;
        &lt;/Group&gt;
      ))}
      &lt;Button onClick={addTask}&gt;Добавить задачу&lt;/Button&gt;
    &lt;/div&gt;
  );
}

При рендере элементов, полученных из 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&lt;ProfileState&gt;({
    name: "",
    age: 0,
    city: "",
  });
 
  return (
    &lt;div&gt;
      &lt;TextInput
        label="Имя"
        value={state.name}
        onChange={(e) =&gt; setState({ name: e.currentTarget.value })}
      /&gt;
      &lt;NumberInput
        label="Возраст"
        value={state.age}
        onChange={(age) =&gt; setState((current) =&gt; ({ age: Number(age) || 0 }))}
      /&gt;
    &lt;/div&gt;
  );
}
Робот собирает из блоков сайт

Собери свой код. Запусти сайт!

От наброска на салфетке до первого работающего лендинга. Наш онлайн-курс «Веб-верстка с нуля и до профессионала» — это интенсивный трек, где ты не будешь зубрить теорию, а с первого дня начнешь превращать идеи в чистый 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 — с их помощью состояние, которое мы сегодня научились аккуратно обновлять, можно будет сохранять прямо в браузере и восстанавливать после перезагрузки страницы.

Теги: