JavaRush /Курсы /Модуль 3: React /Типизация состояния в useState — использование интерфейсо...

Типизация состояния в useState — использование интерфейсов для состояния

Модуль 3: React
1 уровень , 9 лекция
Открыта

Почему важно типизировать состояние?

Давайте начнем с мотивации. Вот вы используете useState, изменения состояния происходят, но потом бац — вы случайно присваиваете значение неправильного типа, что приводит к ошибке где-нибудь в приложении. Ищем, разбираемся, дебажим… А TypeScript-то мог бы сразу сказать: "Эй, ты, это не тот тип данных!".

Типизация состояния гарантирует:

  • Стабильность кода: меньше ошибок, связанных с неправильным использованием типа данных.
  • Простоту поддержки: новичкам в команде будет проще работать с вашим кодом.
  • Автодополнение: мощный инструмент для ускорения разработки.

Введение в типизацию состояния

Помните простой пример с useState, который мы разбирали в прошлой лекции? Если вы забыли, я напомню:

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);

  return (
    <div>
      <p>Счетчик: {count}</p>
      <button onClick={increment}>Увеличить</button>
    </div>
  );
};

На первый взгляд, все работает круто. Но что произойдет, если вы случайно присвоите состояние строкой? Например:

setCount("Ошибка"); // Ups! Ошибка не будет замечена до выполнения кода.

Чтобы предотвратить такие ситуации, мы можем указать тип состояния прямо в useState.

Типизация примитивного состояния

Воспользуемся возможностью TypeScript и добавим тип:

const [count, setCount] = useState<number>(0);

Теперь, если попытаться передать строку в setCount, TypeScript сразу подскажет: "Так нельзя, друг!". Плюс, IDE предложит подсказки, потому что четко знает, что count — это число.

Вот весь компонент с типизацией:

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState<number>(0); // Указали тип: число

  const increment = () => setCount(count + 1);
  const reset = () => setCount(0);

  return (
    <div>
      <p>Счетчик: {count}</p>
      <button onClick={increment}>Увеличить</button>
      <button onClick={reset}>Сбросить</button>
    </div>
  );
};

export default Counter;

Круто? А ведь это только начало!

Типизация сложных состояний

Примитивные типы — это легко. Но что делать, если наше состояние — это объект или массив? Например, у нас есть форма с несколькими полями:

const [formData, setFormData] = useState({
  name: "",
  email: "",
});

Здесь все работает, но мы теряем всю прелесть TypeScript: подсказки и защиту от ошибок. Давайте создадим интерфейс для описания состояния.

Интерфейсы для состояния

Опишем состояние формы через интерфейс:

interface FormData {
  name: string;
  email: string;
}

Теперь передадим его в useState:

const [formData, setFormData] = useState<FormData>({
  name: "",
  email: "",
});

Если вы попытаетесь добавить в formData поле, которого нет в интерфейсе, TypeScript сразу выдаст ошибку. То же самое произойдет, если случайно присвоить неправильный тип значению.

Изменение сложного состояния

Для изменения объекта состояния используйте копию объекта с помощью оператора spread. Например:

const updateName = (newName: string) => {
  setFormData(prevState => ({
    ...prevState,
    name: newName,
  }));
};

Полный пример:

import React, { useState } from 'react';

interface FormData {
  name: string;
  email: string;
}

const Form = () => {
  const [formData, setFormData] = useState<FormData>({
    name: "",
    email: "",
  });

  const updateName = (newName: string) => {
    setFormData(prevState => ({
      ...prevState,
      name: newName,
    }));
  };

  return (
    <form>
      <input
        type="text"
        value={formData.name}
        onChange={e => updateName(e.target.value)}
      />
      <p>Имя: {formData.name}</p>
    </form>
  );
};

export default Form;

Массивы в состоянии

Когда состояние — это массив, принцип работы тот же. Например, список задач:

interface Task {
  id: number;
  title: string;
  completed: boolean;
}

const [tasks, setTasks] = useState<Task[]>([]); // Массив задач

Теперь, добавляя или удаляя задачи, нужно следить, чтобы массив содержал объекты типа Task. Например, добавление новой задачи:

const addTask = (title: string) => {
  const newTask: Task = {
    id: tasks.length + 1,
    title,
    completed: false,
  };
  setTasks([...tasks, newTask]);
};

Полный пример:

import React, { useState } from 'react';

interface Task {
  id: number;
  title: string;
  completed: boolean;
}

const TaskList = () => {
  const [tasks, setTasks] = useState<Task[]>([]);

  const addTask = (title: string) => {
    const newTask: Task = {
      id: tasks.length + 1,
      title,
      completed: false,
    };
    setTasks([...tasks, newTask]);
  };

  return (
    <div>
      <button onClick={() => addTask("Новая задача")}>Добавить задачу</button>
      <ul>
        {tasks.map(task => (
          <li key={task.id}>{task.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default TaskList;

Типизация состояний со значением null

Иногда состояния начинаются с null, а позже инициализируются другим значением. Например:

const [user, setUser] = useState<User | null>(null);

Здесь пользователь сначала отсутствует, но позже данные, например, подгружаются с API. TypeScript строго контролирует обе ситуации — когда user равен null и когда это объект.

Пример:

interface User {
  id: number;
  name: string;
}

const [user, setUser] = useState<User | null>(null);

const loadUser = () => {
  setUser({ id: 1, name: "Иван" });
};

return (
  <div>
    {user ? <p>Пользователь: {user.name}</p> : <p>Нет пользователя</pм}
    <button onClick={loadUser}>Загрузить пользователя</button>
  </div>
);

Типизация сложных вложенных объектов

Если вы работаете с состоянием, содержащим сложные вложенные объекты, то интерфейсы спасают еще сильнее. Например:

interface Address {
  city: string;
  zip: string;
}

interface UserProfile {
  name: string;
  age: number;
  address: Address;
}

const [profile, setProfile] = useState<UserProfile>({
  name: 'Иван',
  age: 30,
  address: {
    city: 'Москва',
    zip: '123456',
  },
});

При обновлении вложенного поля address нужно быть осторожным и копировать нужные уровни объекта:

const updateCity = (city: string) => {
  setProfile(prev => ({
    ...prev,
    address: {
      ...prev.address,
      city,
    },
  }));
};

Типичные ошибки при типизации состояния и как их избежать

  1. Ошибки с null: часто забывают учитывать возможность null. Используйте объединение типов | null, если значение может быть пустым.
  2. Неполная типизация объектов: при обновлении состояния иногда "забывают" про свойства, не указанные явно в интерфейсе, что ведет к потере данных. Старайтесь всегда обновлять копии объектов через spread-оператор.
  3. Неиспользование интерфейсов: когда состояния растут, лучше описывать их интерфейсами — это сделает сложные объекты предсказуемыми.

Теперь у вас есть все инструменты для безопасной и удобной работы с состоянием в React. Давайте двигаться дальше и укреплять наши навыки!

1
Задача
Модуль 3: React, 1 уровень, 9 лекция
Недоступна
Типизация простого состояния
Типизация простого состояния
1
Задача
Модуль 3: React, 1 уровень, 9 лекция
Недоступна
Типизация объекта в состоянии
Типизация объекта в состоянии
3
Опрос
Типизация компонентов с TypeScript, 1 уровень, 9 лекция
Недоступен
Типизация компонентов с TypeScript
Типизация компонентов с TypeScript
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ