Учимся использовать логику компонентов для создания многоразовых хуков

Хуки React, впервые появившиеся в версии React v16.8, кардинально изменили подход к написанию кода. Из коробки React предлагает пользователю доступ к ряду разнообразных хуков, таких как useState, useEffect, useReducer и многим другим, однако имеется возможность создавать и свои варианты, применяя более сложную логику состояний.

Что такое «пользовательские хуки»

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

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

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

useLocalStorageState

Посмотрим на этот компонент Counter, который хранит текущее значение count в локальном хранилище:

import React, { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(() => {
    let value;
    try {
      value = JSON.parse(window.localStorage.getItem('count') || '0');
    } catch (e) {
      value = 0;
    }
    return value;
  });
  
  useEffect(() => {
    window.localStorage.setItem('count', count);
  }, [count]);
  
  return (
    <div>
      <button onClick={() => setCount(prev => prev + 1)}>{count}</button>
    </div>
  );
}

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

useLocalStorageState принимает два параметра: ключ и значение по умолчанию. При первой инициализации программа принимает счётчик count из локального хранилища, если он существует, и задаёт ему значение состояния. В противном случае она возвращает значение по умолчанию. Затем мы используем хук useEffect, благодаря которому локальное хранилище синхронизируется с локальным состоянием, и возвращаем функции state и setState как массив. Обратите внимание, что мы преобразуем значения в строку и парсим их из localStorage, используя JSON.stringify и JSON.parse. Это значит, что с помощью этого хука мы можем хранить в том числе и объекты.

import React, { useState, useEffect } from 'react';

const useLocalStorageState = (key, defaultValue) => {
  const [state, setState] = useState(() => {
    let value;
    try {
      value = JSON.parse(window.localStorage.getItem(key) || defaultValue);
    } catch(e) {
      value = defaultValue;
    }
    return value;
  });
  
  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(state));
  }, [state])
  
  return [state, setState];
}

const Counter = () => {
  const [count, setCount] = useLocalStorageState('count', 0);
  
  return (
    <div>
      <button onClick={() => setCount((prev) => prev + 1)}>{count}</button>
    </div>
  )
}

Теперь можно использовать хук useLocalStorageState в любом доступном компоненте.

useArray

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

import { useCallback, useState } from 'react';

const useArray = (initialValue) => {
  const [value, setValue] = useState(initialValue);
  return {
    value,
    setValue,
    add: useCallback(a => setValue((v) => [...v, a])),
    clear: useCallback(() => setValue(() => [])),
    removeById: useCallback(id => 
      setValue(arr => arr.filter(v => v && v.id !== id))
    ),
    removeIndex: useCallback(index => 
      setValue(v => {
        v.splice(index, 1);
        return v;
      })
    ),
  }
}

Этот хук выглядит довольно просто. Он возвращает объект со множеством модификаторов для состояния массива, с помощью которого можно легко им манипулировать. Мы берём исходный массив в качестве аргумента хука, затем добавляем add, clear, removeById и removeIndex как дополнительные функции помимо обычных значений и операций setValue.

А вот как можно реализовать этот хук внутри компонента:

import React from 'react';

const Todos = () => {
  const todos= useArray(['Take out laundry', 'Walk dog', 'Cook dinner']);
  return (
    <section>
      <h1>Todos</h1>
      <button onClick={() => todos.add('New Todo')}>
        Add Todo
      </button>
      <ul>
      {
        todos.values.map((todo, i) => (
          <li key={i}>
            {todo}
            <button onClick={() => todos.removeIndex(i)}>
              Remove
            </button>
          </li>
        ))
      }
      </ul>
      <button onClick={todos.clear}>
        Clear Todos
      </button>
    </section>
  )
}

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

Обязательно ли ставить use в начале пользовательских хуков

Как гласит документация React:

«Конечно, стоит. Это очень важный элемент. Без него было бы трудно автоматически проверять нарушение правил, потому что невозможно бы было сказать наверняка, содержит ли определённая функция внутри себя вызов хуков».

Пользовательские хуки не копируют состояние

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

Заключение

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

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Caelin Sutch: How to Write a Custom React Hook