Головна / Статті / Набір користувацьких гаків для повторного використання для кожного нового проекту 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);

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

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

Кастомний Hook об’єднує поведінку, а не спільний сховище даних.

Як конкретний приклад, уявіть собі додаток, який має відображати, чи є у користувача наразі підключення до мережі. Замість того, щоб дублювати логіку відстеження подій браузера в кожному компоненті, який її потребує, ви можете загорнути її у кастомний Hook:

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>
  );
}

Інший компонент може використовувати той самий Hook, щоб повністю змінити своє відображення:

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.