Irbisa · Шпаргалка

Эффекты и рефы

useEffect и зависимости, cleanup, гонки запросов, когда эффект не нужен, useRef, useLayoutEffect, двойной запуск в StrictMode

Middle-разработчик · 19 вопросов

  1. 01

    Для чего нужен useEffect и когда он выполняется?

    useEffect синхронизирует компонент с чем-то за пределами React: подпиской, таймером, браузерным API, сетевым соединением.

    function ChatRoom({ roomId }: { roomId: string }) {  useEffect(() => {    const connection = createConnection(roomId);    connection.connect();    return () => connection.disconnect(); // runs before the next effect and on unmount  }, [roomId]); // reconnect only when roomId changes…
  2. 02

    Как работает массив зависимостей useEffect: без массива, с пустым массивом и со списком значений?

    Без массива эффект выполняется после каждого рендера; с [] он выполняется один раз после монтирования, плюс очистка при размонтировании; с [a, b] он выполняется после монтирования и после каждого рендера, в котором изменились a или b.

    useEffect(() => {  document.title = `${count} new messages`;}); // after every render useEffect(() => {  const id = setInterval(refreshClock, 1000);…
  3. 03

    Что такое функция очистки (cleanup) у эффекта и когда React её вызывает?

    Функция очистки возвращается из эффекта и отменяет то, что эффект настроил: отписывается, очищает таймер, отменяет запрос.

    function useWindowWidth() {  const [width, setWidth] = useState(window.innerWidth);   useEffect(() => {    const onResize = () => setWidth(window.innerWidth);    window.addEventListener('resize', onResize);…
  4. 04

    Что такое useRef и чем он отличается от стейта?

    useRef(initial) возвращает изменяемый объект { current }, который живёт столько же, сколько компонент, и сохраняет ту же идентичность между рендерами.

    function Stopwatch() {  const [elapsed, setElapsed] = useState(0); // shown on screen: state  const intervalRef = useRef<number | null>(null); // not shown: ref   function start() {    if (intervalRef.current !== null) return;…
  5. 05

    Как получить доступ к DOM-элементу через ref и как передать ref в свой компонент в React 19?

    Создайте ref через useRef<HTMLInputElement>(null) и передайте его в проп ref элемента; после коммита ref.current содержит DOM-узел, а при размонтировании снова становится null.

    function TextInput({ ref, ...props }: React.ComponentProps<'input'>) {  return <input ref={ref} className="input" {...props} />;} function SearchForm() {  const inputRef = useRef<HTMLInputElement>(null);…
  6. 06

    Почему в режиме разработки эффекты срабатывают дважды и что с этим делать?

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

  7. 07

    Что такое гонка (race condition) при загрузке данных в useEffect и как её предотвратить?

    Когда зависимость быстро меняется (ввод в поиске, переключение вкладок), в полёте оказывается несколько запросов, и более медленный старый ответ может прийти последним и перезаписать новый.

    type Hit = { id: string; title: string }; function SearchResults({ query }: { query: string }) {  const [results, setResults] = useState<Hit[]>([]);   useEffect(() => {…
  8. 08

    Когда эффект не нужен и что использовать вместо него?

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

  9. 09

    Найдите баг: этот эффект крутится в бесконечном цикле и без остановки шлёт запросы.

    function Products({ category }: { category: string }) {  const [products, setProducts] = useState<Product[]>([]);  const filters = { category, inStock: true };   useEffect(() => {    fetchProducts(filters).then(setProducts);  }, [filters]);   return <ProductGrid items={products} />;}

    filters при каждом рендере оказывается новым объектом, поэтому зависимость всегда отличается по Object.is: эффект делает запрос, setProducts сохраняет новый массив, компонент перерисовывается, создаёт новый filters, и цикл повторяется.

  10. 10

    Найдите баг: React пишет «useEffect must not return anything besides a function», а при смене userId страница становится пустой.

    function Profile({ userId }) {  const [user, setUser] = useState(null);   useEffect(async () => {    const response = await fetch(`/api/users/${userId}`);    setUser(await response.json());  }, [userId]);   return <h1>{user ? user.name : 'Loading...'}</h1>;}

    Колбэк эффекта объявлен как async, поэтому возвращает промис, а всё, что возвращает эффект, React считает функцией очистки.

  11. 11

    Что выведет этот компонент в консоль при монтировании и после одного клика (без StrictMode)?

    function Logger() {  const [count, setCount] = useState(0);  console.log('render', count);   useEffect(() => {    console.log('effect', count);    return () => console.log('cleanup', count);  }, [count]);   return <button onClick={() => setCount(count + 1)}>{count}</button>;}

    При монтировании: render 0, effect 0.

  12. 12

    Как реализовать хук usePrevious и какие у него ограничения?

    Классическая версия сохраняет значение в ref внутри эффекта, поэтому во время рендера ref ещё хранит значение из предыдущего коммита.

    // classic: the value from the previous renderfunction usePrevious<T>(value: T): T | undefined {  const ref = useRef<T | undefined>(undefined);  useEffect(() => {    ref.current = value;  });…
  13. 13

    Найдите баг: счётчик доходит от 0 до 1 и дальше не растёт.

    function Ticker() {  const [count, setCount] = useState(0);   useEffect(() => {    const id = setInterval(() => {      setCount(count + 1);    }, 1000);    return () => clearInterval(id);  }, []);   return <p>{count}</p>;}

    Колбэк интервала создан в первом рендере и замкнул count = 0, поэтому каждый тик вызывает setCount(0 + 1); после первого тика стейт уже равен 1, и ничего не меняется.

  14. 14

    Чем useEffect отличается от useLayoutEffect и когда нужен второй?

    useLayoutEffect выполняется синхронно после того, как React обновил DOM, но до того, как браузер отрисует кадр; useEffect обычно выполняется после отрисовки.

    function Tooltip({ anchor, text }: { anchor: DOMRect; text: string }) {  const ref = useRef<HTMLDivElement>(null);  const [top, setTop] = useState(anchor.bottom);   useLayoutEffect(() => {    if (!ref.current) return;…
  15. 15

    Что такое useImperativeHandle и когда стоит открывать методы через ref?

    useImperativeHandle(ref, () => handle) задаёт, что получит ref родителя от вашего компонента: небольшой объект с методами вместо сырого DOM-узла.

    type VideoHandle = { play: () => void; pause: () => void }; function Video({ src, ref }: { src: string; ref?: React.Ref<VideoHandle> }) {  const videoRef = useRef<HTMLVideoElement>(null);   useImperativeHandle(ref, () => ({…
  16. 16

    Как закрыть выпадающее меню, когда пользователь кликает за его пределами?

    Пока меню открыто, слушайте pointerdown на document и закрывайте меню, если цель события находится вне его (!ref.current.contains(e.target)).

    function Dropdown({ items }: { items: string[] }) {  const [open, setOpen] = useState(false);  const rootRef = useRef<HTMLDivElement>(null);   useEffect(() => {    if (!open) return;…
  17. 17

    Как использовать внутри React-компонента библиотеку, которая сама управляет своим DOM, например карту или график?

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

    import Chart from 'chart.js/auto'; function SalesChart({ labels, values }: { labels: string[]; values: number[] }) {  const canvasRef = useRef<HTMLCanvasElement>(null);  const chartRef = useRef<Chart<'line', number[], string> | null>(null);…
  18. 18

    Как читать актуальные пропсы или стейт внутри эффекта, не перезапуская его, и что такое useEffectEvent?

    Вынесите нереактивную часть в событие эффекта: const onConnected = useEffectEvent(() => showNotification(theme)).

    import { useEffect, useEffectEvent } from 'react'; function ChatRoom({ roomId, theme }: { roomId: string; theme: 'light' | 'dark' }) {  const onConnected = useEffectEvent(() => {    showNotification('Connected!', theme); // reads the latest theme  });…
  19. 19

    Что такое колбэк-рефы и что в них изменилось в React 19?

    Колбэк-реф представляет собой функцию, переданную в ref: React вызывает её с DOM-узлом, когда узел прикрепляется, и до React 19 вызывал с null, когда узел открепляется.

    function MeasuredBox({ children }: { children: React.ReactNode }) {  const [height, setHeight] = useState(0);   const measure = useCallback((node: HTMLDivElement) => {    const observer = new ResizeObserver(([entry]) => setHeight(entry.contentRect.height));    observer.observe(node);…