React Hooks

Разделы:


ХукОписание
useStateПозволяет работать с состоянием компонента и управлять его рендером
useEffectВыполняет сайд-эффекты, не блокируя рендер
useContextПозволяет обмениваться данными между компонентами на разных уровнях без передачи параметров
useReducerПозволяет вынести бизнес-логику компонента в редьюсер и управлять ею через диспатчи, а также организовывает общее хранилище данных в initialState
useCallbackВозвращает ссылку на функцию (кеширует функцию), при повторном рендере ссылка не изменится, если не были переданы изменённые данные в массив зависимостей
useMemoПринимает функцию, которая возвращает что-либо, выполняет функцию при первом рендере и кеширует результат, при повторном вызове возвращает мемоизированное значение. Если изменился массив зависимостей, то выполнит функцию заново.
useRefПринимает данные любого типа, возвращает объект с свойством current. При рендерах компонента не перезаписывает ref объект и изменение current не влияют на рендер, что позволяет хранить промежуточные данные.
useImperativeHandleПозволяет получить ссылку на функциональный компонент (можно передать объект со стейтом дочерного компонента и управлять им из родительского).
useLayoutEffectвыполняет сайд-эффекты, не блокируя рендер, но блокируя отображение
useDebugValueиспользуется для отображения метки (помечаем вкладку статусом) пользовательских хуков в React DevTools
useIdсоздаёт уникальный идентификатор, связанный с этим конкретным useId вызовом в этом конкретном компоненте
useDeferredValueПозволяет кешировать значения, возвращать старые в отсутствие новых, тем самым откладывает рендер на потом
useTransitionПозволяет вам обновлять состояние, не блокируя пользовательский интерфейс, а также отслеживать статус выполнения startTransition

useState

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


const [count, setCount] = useState(0);
const [isActive, setIsActive] = useState(false);
const [lists, setLists] = useState([]);

Данные в стейте могут быть любыми: объекты, функции, массивы, примитивы и тп.


function Practice() {
  const [isActive, setIsActive] = useState(false);

  const handleButtonClick = () => {
    setIsActive(!isActive);
  }

  return (
    <div className='practice'>
      <button
        className='practice-button'
        onClick={handleButtonClick}>Переключатель</button>
      {
        isActive ? <p className='practice-text'>Текстовое сообщение</p> :
          <Link className='practice-link' to='/'>На главную</Link>
      }
    </div>
  );
}

useEffect

Первым аргументом принимает функцию, вторым аргументом принимает массив зависимостей.

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

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


function Practice(props) {
  const { url } = props;
  const [profile, setProfile] = useState('');

  useEffect(() => {
    const controller = new AbortController();
    //url = 'https://api.github.com/users/coder1x';

    (async () => {
      const response = await fetch(url, {
        signal: controller.signal
      });

      if (response.ok && response.status === 200) {
        setProfile(await response.text());
      }
    })();

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

  return (
    <pre className='practice'>
      {profile}
    </pre>
  );
}

useContext

Принимает объект, который возвращает createContext. Возвращает данные, которые
были переданы в value Provider или createContext (как значение по умолчанию, если не был указан Provider на верхнем уровне), также заставляет компонент перерендерится если данные изменились, для этого нужно использовать useState и менять данные в параметре value у Provider.

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


const Context = createContext({ text: '' });

function Practice() {
  const [data, setData] = useState({ text: 'backend' });

  const handleButtonClick = () => {
    setData({ text: 'frontend' });
  };

  return (
    <Context.Provider value={data}>
      <button onClick={handleButtonClick}>Изменить данные</button>
      <div className='practice'>
        <Text />
      </div>
    </Context.Provider>
  );
}

function Text() {
  const data = useContext(Context);
  return <p>{data.text}</p>;
}

// сначала на экране выведется надпись
// backend, а после нажатия кнопки она сменится на frontend

useReducer

Первым аргументом принимает функцию reducer, вторым initialState. Есть ещё третий,
необязательный аргумент, которым является функция init (initialState), она вычисляет, каким будет начальное состояние, если её нет то за начальное состояние будет взят initialState.

Возвращает массив [state, dispatch], где state — это текущее состояние, а dispatch — это функция, при вызове которой мы можем менять это состояние.

useReducer — это аналог Redux, он позволяет создать общее хранилище с бизнес логикой.

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


const Context = createContext({});
const initialState = {
  count: 0,
  status: '',
};

function reducer(state, action) {
  const { type, payload } = action;
  const { count, status } = state;

  switch (type) {

    case 'DECREMENTED':
      return {
        count: count ? count - 1 : 0,
        status: count ? payload : 'Ноль',
      }

    case 'INCREMENTED':
      return {
        count: count + 1,
        status: payload,
      }

    default:
      return state;
  }
}

function Practice() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <Context.Provider value={{ dispatch, state }}>
      <div className='practice'>
        <Calc />
      </div>
    </Context.Provider>
  );
}

function Calc() {
  const { dispatch, state } = useContext(Context);

  const handleDecrementedClick = () => {
    dispatch({
      type: 'DECREMENTED',
      payload: 'уменьшил на единицу'
    });
  }

  const handleIncrementedClick = () => {
    dispatch({
      type: 'INCREMENTED',
      payload: 'увеличил на единицу'
    });
  }

  return (
    <>
      <p>Счётчик: {state.count}</p>
      <p>Статус: {state.status}</p>
      <button onClick={handleIncrementedlick}>Увеличить</button>
      <button onClick={handleDecrementedClick}>Уменьшить</button>
    </>
  );
}

useCallback

Возвращает ссылку на функцию (кеширует функцию), при повторном рендере ссылка не изменится, если не были переданы изменённые данные в массив зависимостей.

Обычно используется, когда мы передаём дочерним компонентам callback функцию в
виде параметров, если мы это сделаем без useCallback, то функция, объявленная в родительском компоненте будет вновь создана при следующем рендере, что изменит на неё ссылку и заставит переотрисоваться дочерний компонент.


const Test = memo((props) => {

  return (
    <button onClick={props.callback}>Значение по умолчанию</button>
  );
});

function Practice(props) {
  const { num } = props;
  const [count, setCount] = useState(1);

  const handleTestClick = useCallback(() => {
    setCount(num);
  }, [num]);

  return (
    <div className='practice'>
      <p>Счётчик: {count}</p>
      <button onClick={() => setCount(count + 1)}>Прибавить</button>
      <Test callback={handleTestClick} />
    </div>
  );
}

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


useMemo

Принимает функцию, которая возвращает что-либо, второй аргумент — массив зависимостей. Возвращает мемоизированное значение.

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


function Practice(props) {
  const { num } = props;
  const [count, setCount] = useState(1);

  const numbers = useMemo((lists = []) => {
    for (let i = 0; i < 1000; i++) {
      lists.push(num);
    }
    return lists;
  }, [num]);

  return (
    <div className='practice'>
      <p>Числа: {JSON.stringify(numbers)}</p>
      <button onClick={() => setCount(count + 1)}>Изменить стейт</button>
    </div>
  );
}

При первом рендере компонента Practice, useMemo выполнит callback функцию, закеширует результат и вернёт его переменной numbers в виде массива данных размерностью от 0 до 1000.

При клике на кнопку [Изменить стейт] мы заставляем компонент Practice перерендериться,  при этом callback функция в useMemo не будет выполнена, так как данные в массиве зависимостей  не были изменены, а значит выполнение переданной функции будет иметь такой же результат, поэтому  значения будут взяты из кеша — что сильно увеличивает производительность при повторных рендерах.


useRef

Принимает данные любого типа, возвращает объект с свойством current.

Созданный объект через useRef можно передать в свойство Jsx элемента ref={inputRef}, при этом в свойство current будет записана ссылка на DOM-элемент (только после выполнения рендера) на котором мы, к примеру, сможем отловить или вызвать события фокуса ( inputRef.current.focus(); ), узнать размеры элемента и т.п.

Не записывайте и не читайте ref.current во время рендеринга, за исключением инициализации. Это делает поведение вашего компонента непредсказуемым.


function Practice() {
  const [text, setText] = useState('');
  const inputRef = useRef(null);
  const dataRef = useRef(0);

  const handleInputChange = (event) => {
    const inputText = event.target.value;
    setText(inputText);
    dataRef.current = inputText.length;
  }

  useEffect(() => {
    inputRef.current.focus();
  }, [inputRef]);

  return (
    <div className='practice'>
      <p>Количество знаков: {dataRef.current}</p>
      <label>Введите текст:
        <input ref={inputRef} onChange={handleInputChange} value={text} type='text' />
      </label>
    </div>
  );
}
inputRef — передаём jsx элементу, он записывает в свойство current ссылку на DOM элемент, благодаря этому мы вызываем экшен фокуса на элементе, когда он отрисовывается в первый раз.
 
dataRef — демонстрирует возможность хранения и изменения любых данных, которые не будут переназначены при повторных рендерах. Изменение значения в current не влечёт за собой перерисовку компонента, поэтому не стоит использовать это в jsx элементах. Для отрисовки значений лучше использовать useState, а вариант с reference необходим, к примеру, когда мы создаём булевый флаг, который может повлиять на условные проверки или массив зависимостей хуков, или когда мы создаём объект, хранящий данные о внутренних расчётах компонента, которые нужно аккумулировать, изменять на протяжении жизни компонента ,а после размонтирования удалить.

useImperativeHandle

Для начала разберёмся, как работает forwardRef, потому что они используются в паре с хуком useImperativeHandle. forwardRef позволяет получить ссылку на HTML элемент в функциональном компоненте.

Пример использования:


const CustomInput = forwardRef((props, ref) => {
  const [text, setText] = useState('');

  const handleInputChange = (event) => {
    setText(event.target.value);
  }

  return (
    <label>Введите текст:
      <input ref={ref} onChange={handleInputChange} value={text} type='text' />
    </label>
  );
});

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

  useEffect(() => {
    inputRef.current.focus();
  }, [inputRef]);

  return (
    <div className='practice'>
      <CustomInput ref={inputRef} />
    </div>
  );
}

Как видно из примера, CustomInput получил новое свойство ref, куда мы можем передать inputRef и повлиять на HTML-элемент компонента из родительского.

Теперь рассмотрим вариант, как с помощью useImperativeHandle можно получить ссылку на функциональный компонент.

Принимает три параметра:

  1. ref, который попадает в функциональный компонент извне
  2. функцию, которая возвращает объект, который будет записан в ref
  3. массив зависимостей

const Test = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);

  useImperativeHandle(ref, () => {
    return { setCount };
  })

  return (
    <p>Количество знаков: {count}</p>
  );
});

function Practice() {
  const [text, setText] = useState('');
  const inputRef = useRef(null);
  const dataRef = useRef(null);

  const handleInputChange = (event) => {
    const inputText = event.target.value;
    setText(inputText);
    dataRef.current.setCount(inputText.length);
  }

  useEffect(() => {
    inputRef.current.focus();
  }, [inputRef]);

  return (
    <div className='practice'>
      <Test ref={dataRef} />
      <label>Введите текст:
        <input ref={inputRef} onChange={handleInputChange} value={text} type='text' />
      </label>
    </div>
  );
}

В итоге в родительском компоненте мы по ref-ссылке получим доступ к стейту дочернего компонента. В объект можно передавать любые данные и управлять ими из родительского компонента.


useLayoutEffect

Аналог useEffect, только работает иначе, он блокирует браузер от перерисовки, пока не отработает полностью. В большинстве случаев в нём нет нужды, так как он блокирует отображение, но также есть ситуации, когда отображение зависит от результата выполненного алгоритма.

К примеру нам нужно узнать, какой будет размер DOM элемента после того, как в нём появится контент, этот размер нам необходим для правильного расчёта и позиционирования другого элемента относительно текущего. Если делать эту операцию с помощью useEffect, то сначала мы увидим неспозиционированный элемент (курсор), а потом после просчёта он встанет на правильную позицию, в итоге можно наблюдать эффект дёрганья, мерцания.


function sleep(milliseconds) {
  const time = Date.now() + milliseconds;
  while (Date.now() < time) {
  }
}

function Practice(props) {
  const { text } = props;
  const refText = useRef(null);
  const refTip = useRef(null);
  const [tipWidth, setTipWidth] = useState(0);

  useLayoutEffect(() => {
    const { width } = refText.current.getBoundingClientRect();
    const widthTip = refTip.current.getBoundingClientRect().width / 2;

    sleep(2000);

    setTipWidth(width / 2 - widthTip);
  }, [text]);

  sleep(200);

  return (
    <div className='practice'>
      <p ref={refText}>{text}</p>
      <span ref={refTip} style={{ position: 'absolute', left: `${tipWidth}px` }}>||</span>
    </div>
  );
}
Функция sleep тут только для наглядного сравнения как будет происходить отображение с useEffect и useLayoutEffect. В нашем случае сначала выполнится рендер, jsx элементы запишут ссылку в ref, после выполнится useLayoutEffect, где мы изменим стейт, благодаря чему рендер будет вызван повторно и мы увидим на экране результат. Наш курсор из тега span будет находиться посередине текста.

useDebugValue

Принимает два аргумента:

  1. значение, которое вы хотите отобразить в React DevTools. Оно может иметь любой тип
  2. функция форматирования. Когда компонент проверен, React DevTools вызовет функцию форматирования с аргументом (value), а затем отобразит возвращенное форматированное значение (которое может иметь любой тип)

Для того, чтобы увидеть работу этого хука, нужно установить в ваш браузер расширение React Developer Tools.

useDebugValue может использоваться для отображения метки (помечаем вкладку статусом) пользовательских хуков в React DevTools.


function useCustomHook() {
  const [isOnline, setIsOnline] = useState(false);

  useDebugValue(isOnline ? 'В сети' : 'Не в сети', (value) => value.toUpperCase());

  return isOnline;
}

В результате мы увидим: CustomHook «НЕ В СЕТИ»


useId

useId создаёт уникальный идентификатор, связанный с этим конкретным useId вызовом в этом конкретном компоненте.

useId не предназначен для генерации ключей в списке. Ключи должны быть сгенерированы из ваших данных.


function Practice() {
  const id = useId();

  return (
    <div className='practice'>
      <label>
        Пароль:
        <input type="password" aria-describedby={id} />
      </label>
      <p id={id}>Пароль должен быть не короче 8 символов</p>
    </div>
  );
}

Такие атрибуты доступности HTML , как aria-describedby, позволяют указать, что два тега связаны друг с другом. Например, вы можете указать, что определенный элемент (например, ввод) описывается другим элементом (например, абзацем).

Мы можем на одной странице создать несколько экземпляров <Practice /> и у них у всех будут уникальные id, что не вызовет конфликта.

id выглядит примерно так: ':r0:' или ':r1:' или ':r2:' и т.д.


useDeferredValue

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

useDeferredValue позволяет кешировать значения, возвращать старые в отсутствие новых, тем самым откладывает рендер на потом.
Если используем этот хук, то не нужно использовать useTransition.

Применение:

  • Отображение устаревшего контента во время загрузки нового контента
  • Указание на то, что содержимое устарело
  • Отсрочка повторного рендеринга

function Practice() {
  const [text, setText] = useState('');
  const deferred = useDeferredValue(text);

  const isLoading = useRef(true);
  isLoading.current = true;

  const list = useMemo(() => {
    const dataList = [];

    isLoading.current = false;

    for (let i = 0; i < 10000; i++) {
      dataList.push({
        id: i,
        text: deferred,
      });
    }

    return dataList;
  }, [deferred]);

  const handleInputChange = (event) => {
    setText(event.target.value);
  };

  return (
    <div className='practice'>
      <label> Введите текст:
        <input type="text" value={text} onChange={handleInputChange} />
      </label>
      {
        isLoading.current ? <span>🔄 Загрузка...</span> :
          <ul>
            {list.map((item) => {
              return <li key={item.id}>{item.text}</li>;
            })}
          </ul>
      }
    </div>
  );
}
Когда мы быстро печатаем, в deferred могут оказаться старые данные, так как рендер не успевает завершиться с новыми, соответственно useMemo не будет выполнять функцию со сложным алгоритмом а просто вернёт предыдущий результат, что сильно увеличивает производительность и отзывчивость ввода.

useTransition

Возвращает массив с двумя элементами:

  • isPending — указывает, когда стоит показать процесс ожидания загрузки
  • startTransition — функция, которая позволяет обернуть другую функцию и пометить её как не особо важную операцию, тем самым будет выполнять её и все последующие потоки параллельно, не заставляя рендер ждать выполнение

function Practice() {
  const [text, setText] = useState('');
  const [list, setList] = useState([])
  const [isPending, startTransition] = useTransition()

  const handleInputChange = (event) => {
    const { value } = event.target;

    setText(value);

    startTransition(() => {
      const dataList = [];

      for (let i = 0; i < 10000; i++) {
        dataList.push({
          id: i,
          text: value,
        });
      }

      setList(dataList);
    });
  };

  return (
    <div className='practice'>
      <label> Введите текст:
        <input type="text" value={text} onChange={handleInputChange} />
      </label>
      {
        isPending ? <span>🔄 Загрузка...</span> :
          <ul>
            {list.map((item) => {
              return <li key={item.id}>{item.text}</li>;
            })}
          </ul>
      }
    </div>
  );
}
При каждом вызове handleInputChange будет также выполняться и startTransition вместе с функцией, которая ей передана, но её выполнение никак не будет блокировать рендер и приоритетные процессы, такие как ввод с клавиатуры, поэтому интерфейс будет по-прежнему оставаться отзывчивым. А пока операции в startTransition не завершены, isPending будет иметь значение true, что означает процесс выполнения операции.