Главная / Статьи / Набор персонализированных гаков для повторного использования для каждого нового проекта React

Набор персонализированных гаков для повторного использования для каждого нового проекта React

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

3840 слов

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

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

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

import { useState } from "react";
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : initialValue;
  });  const updateValue = (newValue) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  };  return [value, updateValue];
}

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

Далее идет useToggle, предназначенный для распространённого случая, когда состояние просто переключается между true и false.

import { useState } from "react";
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);  const toggle = () => setValue(v => !v);  return [value, toggle];
}

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

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

import { useState, useEffect } from "react";
function useDebounce(value, delay = 500) {
  const [debounced, setDebounced] = useState(value);  useEffect(() => {
    const timer = setTimeout(() => {
      setDebounced(value);
    }, delay);    return () => clearTimeout(timer);
  }, [value, delay]);  return debounced;
}

Откладывая обновление до момента паузы в вводе, сокращается количество ненужных запросов к API.

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

import { useState, useEffect } from "react";
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });  useEffect(() => {
    const resize = () =>
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });    window.addEventListener("resize", resize);    return () => window.removeEventListener("resize", resize);
  }, []);  return size;
}

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

import { useEffect, useRef } from "react";
function usePrevious(value) {
  const ref = useRef();  useEffect(() => {
    ref.current = value;
  }, [value]);  return ref.current;
}

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

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

import { useEffect } from "react";
function useClickOutside(ref, callback) {
  useEffect(() => {
    function handleClick(e) {
      if (ref.current && !ref.current.contains(e.target)) {
        callback();
      }
    }    document.addEventListener("mousedown", handleClick);    return () =>
      document.removeEventListener("mousedown", handleClick);
  }, [ref, callback]);
}

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

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

import { useEffect } from "react";
function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]);
}

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

useFetch преобразует базовую загрузку данных в повторно используемый хук.

import { useState, useEffect } from "react";
function useFetch(url) {
  const [data, setData] = useState(null);  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData);
  }, [url]);  return data;
}

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

useCopyToClipboard решает проблему повсеместно используемых кнопок копирования.

function useCopyToClipboard() {
  const copy = (text) => {
    navigator.clipboard.writeText(text);
  };
  return copy;
}

Он отлично подходит для быстрого обмена ссылками-приглашениями, кодами купонов или ключами API одним кликом.

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

import { useState, useEffect } from "react";
function useOnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);  useEffect(() => {
    window.addEventListener("online", () => setOnline(true));
    window.addEventListener("offline", () => setOnline(false));
  }, []);  return online;
}

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

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

import { useEffect } from "react";
function useDarkMode(enabled) {
  useEffect(() => {
    document.body.classList.toggle("dark", enabled);
  }, [enabled]);
}

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

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

import { useEffect } from "react";
function useTimeout(callback, delay) {
  useEffect(() => {
    const timer = setTimeout(callback, delay);    return () => clearTimeout(timer);
  }, [callback, delay]);
}

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

Во многих проектах на React постоянно подтверждается один важный урок: не нужно каждый раз создавать всё с нуля. Наличие небольшой библиотеки повторно используемых хуков ускоряет разработку, делает компоненты более понятными и устраняет дублирование кода. Ни один из этих двенадцати хуков не является особо сложным, но вместе они позволяют сэкономить значительное количество времени в течение всего проекта. По мере роста ваших собственных проектов у вас, скорее всего, также появится собственная коллекция подобных инструментов — и важно не общее количество хуков, а то, решают ли они проблемы, с которыми вы постоянно сталкиваетесь. Небольшой утилитный инструмент, который вы напишете сегодня для одного проекта, часто превращается в то, к чему вы обращаетесь в каждом последующем проекте.

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

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

class Counter extends React.Component {
  state = {
    count: 0
  };

  increment = () => {
    this.setState({
      count: this.state.count + 1
    });
  };

  render() {
    return (
      <button onClick={this.increment}>
        {this.state.count}
      </button>
    );
  }
}

С появлением хуков тот же компонент превращается в обычную функцию, которая напрямую вызывает useState:

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

  return (
    <button onClick={() => setCount(c => c + 1)}>
      {count}
    </button>
  );
}

Функциональная версия обычно проще для чтения и повторного использования.

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

if (isLoggedIn) {
  useEffect(() => {
    // ❌
  }, []);
}


for (...) {
  useState(0); // ❌
}

Вместо этого оставьте вызов Hook без условий и разместите условную логику позже:

function Component() {
  const [count, setCount] = useState(0);

  if (count > 10) {
    // Normal conditional logic is fine
  }

  return ...;
}

Это правило существует потому, что React отслеживает состояние по порядку вызовов, а не по именам. Если имеются два вызова useState:

function Component() {
  const [count] = useState(0);
  const [name] = useState("Salim");

  return ...;
}

React сортирует их по положению:

Hook #1 → count
Hook #2 → name

Если первый вызов становится условным:

if (condition) {
  useState(0);
}

useState("Salim");

порядок может меняться между перерисовками:

Render 1:
Hook #1 → count
Hook #2 → name

Render 2:
Hook #1 → name

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

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

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

Он возвращает пару:

count     → current state
setCount  → state update function

Простой счетчик демонстрирует эту схему:

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

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}

Нажатие кнопки запускает эту последовательность:

Click
 ↓
setCount()
 ↓
React schedules update
 ↓
Component renders again
 ↓
New state is calculated
 ↓
DOM is updated if necessary

Состояние — это не обычная переменная вроде let count = 0, поскольку она бы не сохранялась при повторном выполнении функции. React хранит его вне компонента и предоставляет правильное значение при каждой отрисовке:

Render 1
count = 0

Render 2
count = 1

Render 3
count = 2

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

Это важно при нескольких обновлениях состояния в одном обработчике:

setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

Если отрисовка начиналась с значения count, равного 0, все три строки используют один и тот же снимок состояния, в результате чего:

setCount(1)
setCount(1)
setCount(1)

вместо трех реальных увеличений. Решением является функциональное обновление:

setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);

Теперь React применяет каждое обновление поочередно, поскольку каждый функции-обновитель получает самое последнее значение — это полезно, когда новое состояние зависит от старого.

useEffect синхронизирует компонент с чем-то внешним по отношению к React:

useEffect(() => {
  // synchronization logic
}, [dependencies]);

Типичными примерами являются вызовы API, таймеры, обработчики событий, WebSockets, другие API браузера, подписки и библиотеки сторонних разработчиков. Пример:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);

  return <h1>{user?.name}</h1>;
}

Этот эффект синхронизируется с userId; при его изменении эффект должен быть запущен заново.

Такое поведение обусловлено массивом зависимостей. При условии:

useEffect(() => {
  console.log(count);
}, [count]);

React сравнивает зависимости концептуально:

Previous dependencies
        ↓
New dependencies
        ↓
Compare
        ↓
Changed?

Если они отличаются, эффект запускается снова:

Previous: [1]
New:      [2]

→ Effect needs to run

Если они совпадают, его пропускают:

Previous: [2]
New:      [2]

→ Effect can be skipped

Сравнение осуществляется с использованием Object.is, а не глубокого равенства.

Эффекты могут возвращать функцию очистки, которая выполняется перед следующим эффектом и при демонтировании компонента:

useEffect(() => {
  const timer = setInterval(() => {
    console.log("tick");
  }, 1000);

  return () => {
    clearInterval(timer);
  };
}, []);

Функция очистки важна для таких случаев, как:

Timers
Event listeners
Subscriptions
WebSocket connections
Abortable requests

Когда меняются зависимости, React удаляет старый эффект перед запуском нового; он также удаляет его при демонтировании компонента.

Поле поиска показывает это на практике — каждая нажатие клавиши может запустить запрос:

react
react hooks
react performance

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

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  })
    .then(res => res.json())
    .then(setResults)
    .catch(error => {
      if (error.name !== "AbortError") {
        console.error(error);
      }
    });

  return () => {
    controller.abort();
  };
}, [query]);

Цикл жизни выглядит так:

query changes
     ↓
cleanup previous effect
     ↓
abort previous request
     ↓
start new request

В реальных полях поиска это обычно сочетается с техникой дебаунсинга.

Распространенная ошибка — использование useEffect для значений, которые можно получить напрямую:

const [fullName, setFullName] = useState("");

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

Поскольку fullName формируется непосредственно из существующих значений, можно полностью обойтись без эффекта и состояния:

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

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

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

const ref = useRef(initialValue);

Часто им пользуются для обращения к узлу DOM, например для фокусировки поля ввода:

function Input() {
  const inputRef = useRef(null);

  function focusInput() {
    inputRef.current?.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>
        Focus
      </button>
    </>
  );
}

useRef также удобен для хранения изменяемых значений, которые вообще не должны отображаться на экране, таких как идентификатор таймера:

const timerRef = useRef(null);

Затем к нему присваивают значения так же, как и к любому свойству объекта:

timerRef.current = setInterval(...);

Изменение значений

ref.current

само по себе не вызывает повторной перерисовки. Сравните два подхода к пониманию этого явления:

useState
→ update → render

useRef
→ mutate .current → no render

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

useMemo использует иной подход: он хранит в кэше результат вычисления, а не ссылку на DOM.

const result = useMemo(() => {
  return expensiveCalculation(data);
}, [data]);

Можно представить это так: useMemo = кэширование результатов вычислений. Типичный случай применения — фильтрация списка только тогда, когда сами данные действительно изменяются:

function ProductList({ products, search }) {
  const filteredProducts = useMemo(() => {
    return products.filter(product =>
      product.name
        .toLowerCase()
        .includes(search.toLowerCase())
    );
  }, [products, search]);

  return <ProductGrid products={filteredProducts} />;
}

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

Концептуально React хранит небольшую запись, связанную с вызовом гука:

Hook
 ├── memoized value
 └── dependencies

При первой отрисовке:

data = A

calculate(A)
 ↓
result = X

При последующей отрисовке, если data по-прежнему равен A, проверка зависимостей проходит успешно, и React повторно использует X без пересчёта. Только когда data становится чем-то вроде B, React снова выполняет вычисления.

Тем не менее, useMemo легко использовать чрезмерно. Оборачивание кода вот так:

const fullName = useMemo(
  () => `${firstName} ${lastName}`,
  [firstName, lastName]
);

редко того стоит — вычисления здесь тривиальны, а сама мемоизация не бесплатна; она добавляет нагрузку и дополнительный код для обработки. Обращайтесь к useMemo, когда вычисления действительно затратны, когда вам нужен стабильный ссылочный объект или массив, или когда анализ производительности (или логические рассуждения) показывают, что это действительно помогает.

useCallback — это аналогичный инструмент для ссылок на функции вместо их значений.

const handleClick = useCallback(() => {
  doSomething();
}, []);

Это важно, потому что обычные функции создаются заново при каждом отрисовывании:

function Parent() {
  const handleClick = () => {
    console.log("clicked");
  };

  return <Child onClick={handleClick} />;
}

При первом отрисовывании handleClick указывает на один экземпляр функции; при втором отрисовывании — на другой, поэтому эти два экземпляра не являются равными, хотя выполняют одну и ту же задачу.

Это неравенство становится проблемой, когда дочерний компонент оборачивается в React.memo:

const Child = React.memo(function Child({ onClick }) {
  console.log("Child rendered");

  return (
    <button onClick={onClick}>
      Click
    </button>
  );
});

а родительский компонент выглядит так:

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("clicked");
  }, []);

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>
        {count}
      </button>

      <Child onClick={handleClick} />
    </>
  );
}

Когда меняется count:

Parent renders
      ↓
handleClick reference remains stable
      ↓
React.memo checks Child props
      ↓
onClick unchanged
      ↓
Child can skip rendering

Без useCallback только что созданный объект функции будет считаться «новым» для мемизированного дочернего компонента, из-за чего он будет перерисовываться, хотя для него ничего существенного не изменилось.

Тем не менее useCallback — это не то, что следует использовать по умолчанию для каждой функции. Прежде чем оборачивать обработчик, спросите себя, действительно ли что-то выиграет от стабильного объекта функции — например:

React.memo child
Dependency array
Expensive downstream computation

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

Разница между этими двумя хуками заключается в том, что они мемизируют:

useMemo
→ memoizes a VALUE

useCallback
→ memoizes a FUNCTION

Conceptually:

useMemo(() => calculateValue(), deps);

useCallback(() => doSomething(), deps);

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

App
 ↓
Navbar
 ↓
UserMenu
 ↓
Profile
 ↓
Avatar

что приводит к чему-то вроде:

<App user={user} />
<Navbar user={user} />
<UserMenu user={user} />
<Profile user={user} />
<Avatar user={user} />

Такой паттерн передачи пропсов через компоненты, которые иначе им не нуждаются, называется prop drilling.

Настройка контекста начинается с вызова для его создания:

const UserContext = createContext(null);

Затем вы передаете значение с помощью провайдера:

function App() {
  const user = {
    name: "Salim",
    role: "Developer"
  };

  return (
    <UserContext.Provider value={user}>
      <Dashboard />
    </UserContext.Provider>
  );
}

и читаете его там, где он требуется:

function Profile() {
  const user = useContext(UserContext);

  return <h1>Hello {user.name}</h1>;
}

Profile больше не нуждается в user, передаваемом через каждый промежуточный компонент.

Распространенным примером из реальной жизни является тематика:

const ThemeContext = createContext(null);

function App() {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Dashboard />
    </ThemeContext.Provider>
  );
}

Любой компонент может затем напрямую читать текущую тему:

function Button() {
  const { theme } = useContext(ThemeContext);

  return (
    <button className={theme}>
      Submit
    </button>
  );
}

что приводит к формированию структуры, похожей на дерево:

App
 │
 └── ThemeProvider
       │
       └── Dashboard
             │
             └── Button

Button получает значение темы без необходимости прохождения через множество промисов. Стоит отметить, что Context не является автоматическим решением для управления состоянием. Он отвечает на один конкретный вопрос: как сделать так, чтобы значение было доступно компонентам, находящимся глубоко в структуре? Он не предоставляет дополнительных инструментов — селекторов, мидлвэра, структурированных обновлений — которые есть в специализированных библиотеках. Для более сложных задач по управлению состоянием можно воспользоваться:

Redux
Zustand
Jotai
Reducer + Context

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

<ThemeContext.Provider value={theme}>

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

Theme
Locale
Authentication information
Feature flags
Application configuration

Встроенные хуки позволяют упаковывать логику, которую можно использовать повторно, на основе существующих хуков. Простой пример:

function useCounter() {
  const [count, setCount] = useState(0);

  function increment() {
    setCount(c => c + 1);
  }

  return {
    count,
    increment
  };
}

Используется так:

function Counter() {
  const { count, increment } = useCounter();

  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  );
}

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

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

const counterA = useCounter();
const counterB = useCounter();

они не будут делить одно и то же значение count. Каждый вызов получает собственное независимое состояние. Концептуально:

Component A
 └── useCounter
      └── useState → State A

Component B
 └── useCounter
      └── useState → State B

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

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

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(
    navigator.onLine
  );

  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }

    function handleOffline() {
      setIsOnline(false);
    }

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    return () => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, []);

  return isOnline;
}

Теперь любой компонент, который нуждается в информации о состоянии подключения, может использовать её напрямую:

function Navbar() {
  const isOnline = useOnlineStatus();

  return (
    <span>
      {isOnline ? "🟢 Online" : "🔴 Offline"}
    </span>
  );
}

Другой компонент может использовать тот же хук для полной замены своего отображения:

function Checkout() {
  const isOnline = useOnlineStatus();

  if (!isOnline) {
    return <p>You are offline.</p>;
  }

  return <PaymentForm />;
}

Подобный подход применяется и при обработке быстро меняющихся вводных данных с использованием техники дебаунсинга:

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

Этот хук используется в такой функции поиска:

function Search() {
  const [query, setQuery] = useState("");

  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (!debouncedQuery) return;

    // Search API
  }, [debouncedQuery]);

  return (
    <input
      value={query}
      onChange={e => setQuery(e.target.value)}
    />
  );
}

Это приводит к следующей последовательности событий:

User types
    ↓
query changes
    ↓
500ms wait
    ↓
debouncedQuery changes
    ↓
API request

Эти элементы предназначены для объединения, а не для использования изолированно:

                 React Component
                       │
       ┌───────────────┼────────────────┐
       │               │                │
   useState        useEffect        useContext
       │               │                │
    UI state       External systems   Shared data
       │
       └───────────────┐
                       │
                 Custom Hook
                       │
             ┌─────────┼─────────┐
             ▼         ▼         ▼
         useState   useEffect   useMemo

Например, пользовательский хук useProducts() может внутренне использовать:

useState
+
useEffect
+
useMemo

данные из состояния аутентификации, получаемые через useContext. Повторная отрисовка происходит по предсказуемой схеме:

State / Props / Context change
          ↓
      React update
          ↓
       Render
          ↓
   Reconciliation
          ↓
       Commit
          ↓
      DOM update
          ↓
      Browser paint

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

useState
→ provides state + schedules updates

useMemo
→ calculates/reuses values during render

useCallback
→ calculates/reuses function references during render

useContext
→ reads context during render

useEffect
→ synchronizes with external systems after commit

Связанная литература

  • React Components 101: Создание повторно используемых и легко обслуживаемых элементов интерфейса — Узнайте, почему разделение интерфейса на небольшие компоненты React улучшает их повторное использование, читаемость и сотрудничество в команде, а затем создайте свой первый функциональный компонент.
  • Почему catch () вызывает SyntaxError в JavaScript — Узнайте, почему пустой список параметров в блоке catch полностью нарушает парсинг JavaScript, и ознакомьтесь с двумя грамматически корректными способами написания блока catch без параметров.
  • Прекратите синхронизацию состояния с useEffect: более безопасный паттерн в React — Узнайте, почему использование useEffect для синхронизации производного состояния приводит к конкурентным ситуациям и избыточной отрисовке, а также как заменить его на вычисление состояния во время отрисовки и использование атрибута key.