Галоўная / Артыкулы / Кіт інструментаў з перадзначанымі хукамі, якія можна викорыстоўваць знову, для кожнага новага проекту на 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];
}

Гэта ўзгодна для модалаў, меню з выпадаючымі элементамі, бокавых панелей і падобных элементаў UI, якія можна увыключыць.

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 закрывае элементы UI, такія як модалкі, калі корыстнік нажымае будзь-дзе за ўсёю іх тэриторыяй, што адпавядае таму, як корыстнікі інтуўітыўна чакаюць ад гэтых компонентов.

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

Custom Hooks дазволяюць пакаваць логіку, якая можа быть викорыстаная знову, створаную на базе вбудованных hooks. Мінімальны прыклад:

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

Настоямыя перадчынныя тут — па-новаму викорыстанне логіки, а не па-новаму викорыстанне маркапаў.

Це варта падкрэсліць: custom Hooks дзеляцца логікай, а не станам. Якщо два разлучныя компаненты кожны вызываюць той самы custom Hook:

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.