Главная / Статьи

Переосмысление состояния в React: где на самом деле должны храниться ваши данные

Меньше ошибок в React благодаря ограниченному использованию useState: перемещайте состояние в URL, DOM или вычисляемые значения, и определите, кто отвечает за каждую его часть, прежде чем обращаться к инструментам управления состоянием.

Издатель: REACTAPP.TOP17 мин чтения3931 слов

Источники: Stop Adding React State: It’s Creating Bugs You Don’t Need

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

После тщательной фильтрации он выделяет корпоративных клиентов, находящихся в Европе, у которых подписки не продлевались в течение последних девяноста дней.

В результате получается точный список из тридцати учетных записей, требующих немедленного внимания.

Он берет URL прямо из браузера и отправляет его в Slack своему региональному директору с примечанием:

«Вот учетные записи приоритетного значения на сегодняшнее утро».

Директор открывает ссылку.

Страница загружается.

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

Вся эта тщательная фильтрация исчезла, потому что инженер, создавший страницу, хранил каждый фильтр, поле сортировки и значение пагинации во внутреннем состоянии компонента с помощью useState.

Этот пример иллюстрирует паттерн, который постоянно встречается в кодовых базах React.

Мы склонны использовать состояние просто потому, что React делает его создание очень простым.

Нужно значение фильтра?

useState.

Нужно отслеживать, какая вкладка активна?

useState.

Нужен счётчик?

useState.

Нужно сохранить значение поля формы?

useState.

Нужно отслеживать разные этапы вызова API?

Это ещё три вызова useState.

Вскоре один компонент начинает обрабатывать десятки значений, которые изначально вовсе не обязательны были находиться в состоянии React.

Само состояние здесь не является врагом. Каждому сложному приложению на React оно необходимо.

Настоящая проблема заключается в размещении состояния там, где оно не должно находиться.

Каждая дополнительная переменная состояния — это ещё одна вещь, с которой приложению нужно синхронизироваться со всем остальным.

Если этого не контролировать, это может привести к множеству проблем, среди которых:

  • Дополнительные усилия на синхронизацию дублирующихся значений
  • Данные, которые устаревают в каком-либо кэше
  • Значения, отображаемые на экране, которые больше не соответствуют действительности
  • Обновления, происходящие тогда, когда на самом деле ничего существенного не изменилось
  • Проблемы с временем выполнения, когда два обновления сталкиваются непредсказуемо
  • Эффекты, которые становятся всё более запутанными в попытках сохранить целостность данных
  • Баги, поиск причин которых занимает гораздо больше времени
  • Страница, которая забывает всё сразу после перезагрузки
  • Экраны, к которым никто не может сделать ссылку или сохранить их для последующего использования
  • Комбинации флагов, приводящие к состоянию интерфейса, которого вообще не должно существовать
  • Создание отличных приложений на React на самом деле не сводится к овладению сложной логикой состояний.

    Часто речь идет о понимании того, когда вообще не создавать это состояние.

    1. URL — лучший менеджер состояния вашего приложения

    Представьте типичный экран приложения с большим объемом данных.

    Вероятно, ему нужно отслеживать:

    • Поиск
    • Фильтры
    • Сортировка
    • Пагинация
    • Активные вкладки
    • Выбранные категории
    • Диапазоны дат

    Обычно принятый подход — сохранять всё это в состоянии компонента.

    function ProductList() {
      const [activeCategory, setActiveCategory] = useState('all');
      const [currentPage, setCurrentPage] = useState(1);
      const [searchQuery, setSearchQuery] = useState('');
    
      // Fetch and filter data...
    }
    

    На первый взгляд это кажется совершенно нормальным.

    Затем пользователь нажимает «обновить».

    Все значения возвращаются к своим значению по умолчанию.

    Или они делятся ссылкой с коллегой.

    Этот коллега попадает на совершенно другой вид страницы.

    Или они нажимают кнопку «Назад» в браузере.

    Настроенная ими фильтрация может не восстанавливаться, поскольку состояние React не имеет встроенной связи с историей навигации браузера.

    useState сам по себе здесь не виноват.

    Настоящая проблема заключается в том, что URL уже существует как механизм для представления состояния навигации, и его полностью игнорируют.

    Вместо того чтобы писать:

    const [currentPage, setCurrentPage] = useState(1);
    const [searchQuery, setSearchQuery] = useState('');
    const [category, setCategory] = useState('all');
    

    эту же информацию можно закодировать непосредственно в URL.

    Что-то вроде:

    /products?category=enterprise&page=3&query=react
    

    Используя библиотеки маршрутизации, такие как Next.js, React Router или TanStack Router, можно считывать эти параметры прямо из URL:

    import { useSearchParams } from 'next/navigation';
    
    function ProductList() {
      const searchParams = useSearchParams();  const category = searchParams.get('category') || 'all';
      const page = Number(searchParams.get('page')) || 1;
      const query = searchParams.get('query') || '';  // Fetch data using the URL state...
    }
    

    Это небольшое изменение кардинально влияет на жизненный цикл данного состояния.

    Обновить страницу?

    Фильтры останутся прежними.

    Поделиться ссылкой?

    Любой, кто её откроет, увидит точно такой же вид.

    Сохранить как закладку?

    Позже она снова загрузится в то же состояние.

    Нажать «Назад»?

    Браузер правильно переходит к предыдущим состояниям.

    Это значимое преимущество с точки зрения архитектуры.

    Состояние URL дает больше, чем просто сохранение данных

    Возможность выжить после обновления страницы — это лишь часть преимуществ.

    Большее преимущество заключается в том, что URL превращается в портативную копию текущего состояния вашего приложения, которую можно поделиться с кем угодно.

    Возьмём панель управления службой поддержки клиентов в качестве примера:

    /support/tickets?status=open&priority=high&team=billing
    

    Один менеджер может мгновенно отправить эту точную ссылку другому менеджеру.

    Панель управления, ориентированная на продажи, может выглядеть так:

    /accounts?region=europe&plan=enterprise&renewal=90
    

    Инструмент аналитики может использовать что-то вроде:

    /analytics?range=30d&metric=revenue&groupBy=region
    

    Ни один из этих запросов не является произвольным.

    Каждый из них — это глубокая ссылка, ведущая к конкретному, воссоздаваемому состоянию приложения.

    Именно для этого и были созданы URL-адреса.

    Когда состояние URL имеет смысл

    Параметры URL отлично подходят для таких данных, как:

    • То, что пользователь ввёл в поле поиска
    • Активные выборки фильтров
    • Столбцы и направление сортировки
    • Та страница результатов, которая отображается
    • Активная вкладка
    • Выбранный диапазон дат
    • Тот вариант просмотра, который сейчас открыт
    • Настройки генерируемого отчёта
    • Персонализированная конфигурация на панели управления
    • Любое состояние интерфейса, которое пользователь может передать другому человеку

    Тем не менее, не всё подходит для использования в URL.

    Пароли, очевидно, не должны находиться в URL.

    То же самое касается временного состояния модальных анимаций.

    В общем случае — и крупных, глубоко вложенных объектов тоже.

    Вопрос, который нужно задать, прост:

    Спросите себя, ожидает ли пользователь, что этот вид представления можно будет поделиться, добавить в закладки или восстановить — если да, то URL, скорее всего, является подходящим местом для хранения этого состояния.

    2. Прекратите хранить значения, которые можно легко вычислить

    Один из простых способов сокращения объема состояния в React — это перестать сохранять значения, которые на самом деле получаются из других значений.

    Возьмем этот пример:

    const [items, setItems] = useState([]);
    const [itemCount, setItemCount] = useState(0);
    

    А в другом месте компонента:

    setItems(newItems);
    setItemCount(newItems.length);
    

    Теперь у вас есть две отдельные переменные состояния, описывающие один и тот же факт.

    Если items обновляется, но itemCount не меняется, интерфейс будет отображать некорректную информацию.

    Вы создали проблему с синхронизацией, которая вообще не должна была возникнуть.

    Более чистая версия выглядит так:

    const [items, setItems] = useState([]);
    
    const itemCount = items.length;
    

    Теперь существует единый источник истины.

    Подсчёт выполняется каждый раз при отрисовке компонента.

    Та же логика применяется при фильтрации коллекций.

    Вместо того чтобы писать:

    const [items, setItems] = useState([]);
    const [activeItems, setActiveItems] = useState([]);
    

    обычно достаточно просто написать:

    const activeItems = items.filter(item => item.active);
    

    Или:

    const totalPrice = items.reduce(
      (total, item) => total + item.price,
      0
    );
    

    Или:

    const hasErrors = fields.some(field => field.error);
    

    Или:

    const completedCount = todos.filter(todo => todo.completed).length;
    

    Вопрос, который стоит задать, — это не:

    «Могу ли я технически сохранить это в состоянии?»

    Конечно, можете.

    На самом деле следует спросить:

    «Имеет ли это значение собственный жизненный цикл?»

    Если нет, то ему вряд ли место в состоянии.

    3. Прекратите связывать каждое поле формы с состоянием

    Ещё одна привычка, от которой стоит избавиться, — рассматривать каждое поле формы как отдельную переменную состояния.

    Небольшая форма, построенная таким образом, может выглядеть следующим образом:

    function ContactForm() {
      const [firstName, setFirstName] = useState('');
      const [lastName, setLastName] = useState('');
      const [email, setEmail] = useState('');
      const [message, setMessage] = useState('');
    
      const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();    submitData({
          firstName,
          lastName,
          email,
          message
        });
      };  return (
        <form onSubmit={handleSubmit}>
          <input
            value={firstName}
            onChange={e => setFirstName(e.target.value)}
          />      <input
            value={lastName}
            onChange={e => setLastName(e.target.value)}
          />      <input
            value={email}
            onChange={e => setEmail(e.target.value)}
          />      <textarea
            value={message}
            onChange={e => setMessage(e.target.value)}
          />      <button type="submit">Send</button>
        </form>
      );
    }
    

    С технической точки зрения здесь нет ничего неверного.

    Управляемые поля необходимы тогда, когда приложению действительно нужно реагировать на ввод пользователя. Вот несколько ситуаций, когда это важно:

    • Проверка корректности поля по мере его заполнения
    • Предоставление подсказок в стиле автодополнения
    • Мгновенная переработка текста
    • Подсчёт оставшихся символов
    • Отображение или скрытие других полей в зависимости от введённого текста
    • Оценка уровня безопасности пароля
    • Связывание поведения одного поля с значением другого

    Но часто у приложения нет причин интересоваться тем, что ввёл пользователь, до тех пор пока он не нажмёт Отправить.

    В таких случаях React не нужно отслеживать каждый символ по мере его ввода.

    Браузер уже выполняет эту работу.

    Позвольте браузеру справляться с этим

    Встроенные формы HTML уже десятилетиями управляют значениями ввода.

    Вы можете воспользоваться этим встроенным поведением с помощью FormData:

    function ContactForm() {
      const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
    
        const formData = new FormData(e.currentTarget);    const payload = {
          firstName: formData.get('firstName') as string,
          lastName: formData.get('lastName') as string,
          email: formData.get('email') as string,
          message: formData.get('message') as string,
        };    submitData(payload);
      };  return (
        <form onSubmit={handleSubmit}>
          <input name="firstName" required />
          <input name="lastName" required />
          <input name="email" type="email" required />
          <textarea name="message" required />      <button type="submit">Send</button>
        </form>
      );
    }
    

    С использованием этого подхода React больше не нуждается в отдельном поле состояния для каждого поля.

    Браузер сохраняет то, что ввел пользователь.

    React просто считывает значения после отправки формы.

    Это особенно полезно для больших форм.

    Представьте процесс адаптации новых сотрудников в компании, включающий 30 полей.

    Действительно ли вам понадобится 30 отдельных переменных состояния только для хранения введенного текста?

    Вряд ли.

    Контролируемые и неконтролируемые элементы ввода могут сосуществовать

    Все это не означает, что компоненты с контролем — это ошибка.

    Они не такие.

    Существует множество ситуаций, когда контроль ввода действительно необходим.

    Суть заключается просто в том, чтобы отказаться от предположения, что React должен по умолчанию контролировать значение каждого поля.

    Если приложению не нужны данные до момента отправки, часто достаточно позволить стандартной форме хранить их.

    Сделав такое различие, можно значительно упростить работу с большими формами.

    4. Почему логические флаги теряют эффективность

    Ещё одна частая проблема постоянно встречается в реальных кодовых базах.

    Рассмотрим типичный вызов API.

    Распространённым подходом является отслеживание состояния с помощью трёх отдельных логических флагов:

    const [isLoading, setIsLoading] = useState(false);
    const [isSuccess, setIsSuccess] = useState(false);
    const [isError, setIsError] = useState(false);
    

    В начале все три флага имеют значение false:

    loading = false
    success = false
    error = false
    

    Как только начинается запрос:

    loading = true
    

    После успешного завершения запроса:

    loading = false
    success = true
    

    На первый взгляд это кажется разумным.

    Но предположим, что блок обработки ошибок не справляется с сбросом переменной isLoading.

    В итоге может возникнуть следующее:

    loading = true
    error = true
    

    Или небрежный механизм повторных попыток может привести к:

    success = true
    error = true
    

    В таком случае компонент находится в состоянии, которое логически не имеет смысла.

    Должен ли он отображать сообщение о успехе?

    Должен ли он отображать состояние ошибки?

    Должен ли он по-прежнему показывать индикатор загрузки?

    С технической точки зрения все три логических значения могут одновременно оправдывать ответ «да».

    Именно эта неоднозначность и является ошибкой.

    Лучше рассматривать его как конечную машину состояний

    Более чистый подход:

    type RequestStatus =
      | 'idle'
      | 'loading'
      | 'success'
      | 'error';
    
    const [status, setStatus] =
      useState<RequestStatus>('idle');
    

    Теперь каждое допустимое состояние явно определено и взаимоисключает другие.

    Запрос может находиться только в одном из следующих состояний:

    idle
    loading
    success
    error
    

    Невозможно случайно отобразить одновременно состояния «загрузка» и «успех».

    Процесс отрисовки также становится гораздо проще для понимания:

    if (status === 'loading') {
      return <Spinner />;
    }
    
    if (status === 'error') {
      return <ErrorMessage />;
    }if (status === 'success') {
      return <SuccessBanner />;
    }return <DefaultForm />;
    

    Это не просто удобная функция TypeScript.

    Она отражает более общее правило проектирования:

    Когда два состояния никогда не могут быть истинными независимо друг от друга, их не следует моделировать как две отдельные переменные.

    5. Избегайте использования эффектов для синхронизации состояния

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

    Возьмем следующую конфигурацию:

    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [fullName, setFullName] = useState('');
    

    В сочетании с:

    useEffect(() => {
      setFullName(`${firstName} ${lastName}`);
    }, [firstName, lastName]);
    

    То, что вы здесь создали, — это механизм синхронизации для значения, которое изначально вообще не требовало собственного состояния.

    Проще решение:

    const fullName = `${firstName} ${lastName}`;
    

    Изменение может показаться незначительным, но его архитектурное влияние значительно.

    Исходная версия требует:

    • Трех отдельных переменных состояния
  • Эффект для их выравнивания
  • Непрямая связь синхронизации между значениями
  • Дополнительный цикл отрисовки, запускаемый этим эффектом
  • Массив зависимостей, который должен оставаться точным
  • В переработанной версии требуется только:

    • Два реальных значения
    • Одно значение, полученное из них
    • Никаких эффектов
    • Никакой логики синхронизации

    Хорошей привычкой при анализе компонентов React является задавание следующего вопроса:

    "Почему это значение должно находиться вне расчётов отрисовки?"

    Если честный ответ — "чтобы синхронизироваться с чем-то ещё", это обычно признак того, что это значение вообще не должно быть состоянием.

    6. Три вопроса, которые следует задать перед добавлением состояния

    Прежде чем написать эту строку:

    const [value, setValue] = useState(...);
    

    сначала проведите три проверки.

    Вопрос 1: Можно ли вычислить это во время отрисовки?

    Если данные, необходимые для формирования этого значения, уже существуют, нет причин хранить их дубликат.

    Вместо того чтобы:

    const [itemCount, setItemCount] = useState(0);
    

    записывать:

    const itemCount = items.length;
    

    Вместо того чтобы:

    const [hasErrors, setHasErrors] = useState(false);
    

    записывать:

    const hasErrors = fields.some(field => field.error);
    

    Избегайте хранения состояния для того, что можно вычислить на месте.

    Вопрос 2: Должно ли это сохраняться после перезагрузки страницы?

    Если да, подумайте, является ли URL подходящим местом для хранения этих данных.

    Типичные примеры:

    • Поисковые запросы
    • Состояние пагинации
    • Активные фильтры
    • Порядок сортировки
    • Выбранные вкладки
    • Диапазоны дат

    Задайте себе вопрос: если ссылка на эту страницу будет передана другому пользователю, должен ли он увидеть точно тот же вид страницы?

    Если да, то URL обычно является лучшим местом для хранения этих данных.

    Вопрос 3: Может ли браузер хранить эти данные до отправки формы?

    Если значение поля важно только в момент отправки, возможно, достаточно будет неуправляемого ввода.

    Вместо:

    const [email, setEmail] = useState('');
    

    вам, возможно, понадобится просто:

    <input name="email" type="email" />
    

    и затем считать его с помощью:

    const formData = new FormData(e.currentTarget);
    

    Позвольте браузеру управлять тем, что он уже способен управлять.

    7. Что действительно должно находиться в состоянии React

    Ничто из этого не является аргументом в пользу полного устранения состояния компонента.

    Попытка полностью его убрать была бы чрезмерной коррекцией.

    Множество значений действительно подходят для хранения в состоянии.

    Например:

    const [isOpen, setIsOpen] = useState(false);
    

    Временный выпадающий список или модальное окно совсем не обязательно должны отражаться в URL.

    Интерактивное состояние

    const [activeStep, setActiveStep] = useState(1);
    

    Когда значение управляет взаимодействием, которое происходит полностью внутри одного компонента, его хранение в локальном состоянии часто является правильным решением.

    Значения, которые меняются независимо

    Если у чего-то действительно есть собственный жизненный цикл, и невозможно рассчитать его на основе уже имеющихся данных, тогда для хранения такого значения подходит состояние.

    Оптимистичный интерфейс

    Иногда необходимо отобразить что-то на экране до того, как сервер фактически подтвердит произошедшее событие.

    Это тоже законное применение состояния.

    Цель не в том, чтобы следовать этому правилу:

    "Используйте меньше useState, независимо от обстоятельств."

    Цель заключается в следующем:

    "Убедитесь, что у каждого элемента состояния есть соответствующий владелец."

    Иногда правильным владельцем является React.

    Иногда — URL.

    Иногда — DOM.

    Иногда — сервер.

    Иногда — специализированный слой для получения данных или управления состоянием.

    Именно это решение относительно владельца формирует архитектуру приложения.

    8. Сосредоточьтесь на том, кто владеет состоянием, а не только на количестве имеющегося у вас состояния

    Хорошо построенное приложение на React определяется не малым количеством переменных состояния.

    На самом деле важно назначить каждой части состояния ясного и правильного владельца.

    Возьмем панель управления, которая отслеживает:

    Search query
    Filters
    Pagination
    Sort order
    Form values
    Server data
    Loading status
    Selected row
    Modal visibility
    Derived totals
    

    Хочется без раздумий поместить все эти элементы в useState.

    Но у этих значений разные функции, поэтому они не должны находиться в одном месте.

    Более полезная модель предполагает их разделение следующим образом:

    URL

    • Поиск
    • Фильтры
    • Пагинация
    • Порядок сортировки
    • Вкладки для обмена

    DOM

    • Значения формы, важные только после её отправки

    Сервер/слой данных

    • Данные клиентов
    • Ответы API
    • Данные сервера из кэша

    Рассчитанные значения

    • Количества
    • Итоговые значения
    • Фильтрованные списки
    • Краткое резюме проверок

    Состояние React

    • Видимость модального окна
    • Временные выборы
    • Состояние анимации
    • Кратковременные взаимодействия

    Как только состояние распределяется таким образом, у компонента остается гораздо меньше элементов, которые нужно синхронизировать.

    Именно в этом сокращении и кроется настоящая польза.

    9. Дублированное состояние — источник ошибок будущего

    Самое рискованное состояние в приложении не обязательно является самым сложным.

    Часто это состояние, которое существует одновременно в нескольких местах.

    Представьте, что у вас есть:

    const [products, setProducts] = useState([]);
    const [filteredProducts, setFilteredProducts] = useState([]);
    

    Теперь от вас зависит обеспечение того, чтобы filteredProducts всегда совпадал с содержимым products.

    Если добавить ещё один фильтр, получится следующее:

    const [filteredProducts, setFilteredProducts] = useState([]);
    const [searchResults, setSearchResults] = useState([]);
    

    Теперь появляется дополнительная зависимость, которую необходимо вручную синхронизировать.

    Рано или поздно кто-то обновит исходные данные, но забудет обновить одну из производных переменных, зависящих от них.

    Интерфейс тихо теряет синхронизацию с реальностью.

    В конце концов пользователь сообщает что-то вроде:

    "В отчёте указано 42 продукта, но на экране отображаются только 39."

    Такое несоответствие обычно не является ошибкой самой логики фильтрации.

    Это симптом того, что у приложения есть более одного источника истины для одной и той же информации.

    Если же значение вычислять иначе:

    const filteredProducts = products.filter(...);
    

    Вы устраняете целую категорию ошибок ещё до того, как они могут возникнуть.

    Вот почему сокращение количества состояний касается не только производительности отрисовки.

    На самом деле речь идёт о сокращении числа элементов, которые могут выйти из синхронизации.

    10. Расходы на состояние выходят за рамки повторной отрисовки

    Когда люди обсуждают состояние, разговор обычно касается производительности: «Приведёт ли это к ещё одной отрисовке?»

    Это вполне обоснованная проблема, но это не самая дорогостоящая часть всей ситуации.

    Настоящая стоимость — это сложность.

    Каждая дополнительная переменная состояния — это ещё один элемент, который ваша команда должна запоминать.

    Затем неизбежно появляется вызов метода установки значения.

    setSomething(...)
    

    Затем, вероятно, используется useEffect, чтобы отреагировать на это.

    useEffect(...)
    

    Затем — массив зависимостей, в котором указаны значения, за которыми осуществляется наблюдение.

    [firstValue, secondValue]
    

    Затем необходима какая-то логика сброса, чтобы вернуть его к начальному значению.

    setSomething(initialValue)
    

    Затем возникает крайний случай, когда этот состояние должно оставаться синхронизированным с изменением навигации.

    Затем поступает отчёт об ошибках, потому что состояние не сбрасывается после определённых изменений.

    Постепенно кодовая база превращается в сеть логики синхронизации.

    Это гораздо сложнее для понимания, чем компонент, у которого данные поступают чётко из нескольких хорошо определённых источников.

    Настоящий навык в React — это не управление большим количеством состояний

    Это понимание того, где должно находиться каждое элемента состояния.

    Менее опытный разработчик, глядя на интерфейс, может спросить:

    "Какое состояние здесь нужно?"

    Лучшее архитектурное чутьё подсказывает другой вопрос:

    «Какие из этих значений действительно вообще нуждаются в хранении в состоянии?»

    А самая точная формулировка вопроса звучит так:

    «Кто является законным владельцем каждой части состояния?»

    Изменение подхода позволяет совершенно иначе подходить к созданию приложений на React.

    URL может отвечать за состояние навигации.

    Браузер может отвечать за состояние форм.

    Сервер может отвечать за данные, принадлежащие ему.

    Полученные значения могут оставаться производными, вместо того чтобы их дублировать.

    Объединения в TypeScript могут моделировать взаимоисключающие состояния.

    А сам React может сосредоточиться на кратковременном, интерактивном состоянии, которое действительно принадлежит слою UI.

    Такое разделение обязанностей создаёт гораздо более чистую архитектуру.

    Резюме

    Наличие большего количества состояний само по себе не является достижением.

    Увеличение числа переменных состояния не делает приложение более функциональным.

    Обычно это приводит лишь к увеличению объема работы по синхронизации, большему количеству зависимостей, которые необходимо отслеживать, и к повышенной вероятности несоответствия данных.

    Прежде чем использовать еще один элемент useState, ответьте на следующие вопросы:

    1. Можно ли вычислить это значение во время отрисовки? Если да, лучше вычислить его, а не хранить отдельно.
    2. Нужно ли, чтобы оно сохранялось после обновления страницы или могло быть передано по ссылке? Если да, поместите его в параметры поиска URL.
    3. Необходимо ли отслеживать это поле формы при каждом нажатии клавиши? Если нет, возможно, достаточно будет неуправляемого ввода в сочетании с FormData.
  • Описывают ли несколько логических флагов на самом деле один основной жизненный цикл? Объедините их в одно значение статуса или использовайте машину состояний.
  • Создается ли это состояние только для синхронизации с другим элементом состояния? Если да, то, скорее всего, вы храните что-то, что можно было бы просто вычислить.
  • Имеет ли это значение действительно собственный жизненный цикл? Если нет, то ему, вероятно, вообще не нужно быть состоянием.
  • Цель никогда не заключалась в минимизации количества вызовов useState.

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

    Потому что каждый ненужный элемент состояния — это ещё одна вещь, которую приложению нужно синхронизировать.

    А каждый шаг синхронизации, который вы не пишете, — это баг, который вам никогда не придётся исправлять.