Погружение в хуки реагирования с примерами

Если вы являетесь частью мира разработчиков React, концепция хуков должна быть вам знакома. Представленные в React 16.8 хуки стали популярным решением для управления состоянием и побочными эффектами в функциональных компонентах. Эта статья знакомит вас с восемью самыми популярными хуками React и завершается обсуждением создания пользовательских хуков.

1. состояние использования

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

const [state, setState] = useState(initialState);

Например, если бы мы создавали простой счетчик:

const Counter = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
};

Этот хук объявляет переменную состояния count, инициализированную значением 0. Чтобы обновить count, нужно просто позвонить setCount().

2. использоватьЭффект

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

Рассмотрим компонент, который извлекает пользовательские данные из API:

const UserProfile = ({userId}) => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(`https://api.example.com/user/${userId}`);
      setUser(result.data);
    };

    fetchData();
  }, [userId]);  // The effect depends on the userId that is passed as a prop

  return user ? (<h2>{user.name}</h2>) : (<p>Loading...</p>);
};

По умолчанию эффект запускается после каждого рендера. Однако если вы передаете массив зависимостей (userId в примере), эффект запускается только при изменении зависимостей.

3. использовать контекст

useContext позволяет вам получить доступ к контексту без необходимости оборачивать компонент в потребитель контекста. Он принимает объект контекста (значение, возвращенное из React.createContext) и возвращает текущее значение контекста, предоставленное ближайшим поставщиком контекста для данного контекста.

Вот простой компонент переключения темы:

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function ThemeTogglerButton() {
  const theme = useContext(ThemeContext);

  return (
    <button style={{background: theme.background, color: theme.foreground}}>
      Toggle Theme
    </button>
  );
}

4. использовать Редуктор

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

Пример использования — простой счетчик, в котором вы можете увеличивать, уменьшать или сбрасывать значение:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return initialState;
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'reset'})}>Reset</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

Здесь reducer — это функция, которая определяет, как состояние изменяется в ответ на действия, а dispatch — это метод отправки действий редюсеру.

5. использоватьСсылка

useRef возвращает изменяемый объект ref, чье свойство .current инициализируется переданным аргументом (initialValue). Возвращенный объект будет сохраняться в течение всего срока службы компонента. Его можно использовать для прямого доступа к элементам DOM.

Распространенным вариантом использования useRef является императивный доступ к дочернему элементу:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

6. использовать памятку

useMemo будет пересчитывать запомненное значение только при изменении одной из зависимостей. Эта оптимизация помогает избежать дорогостоящих вычислений при каждом рендере.

Рассмотрим компонент, который выполняет дорогостоящие вычисления:

function MyComponent({ a, b }) {
  const result = useMemo(() => {
    let output = 0;
    // Imagine a really expensive computation here
    for (let i = 0; i < 100000000000; i++) {
      output = a + b + i;
    }
    return output;
  }, [a, b]);  // Only re-run the expensive function if `a` or `b` changes

  return <h2>{result}</h2>;
}

7. использоватьОбратный звонок

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

Вот пример с поисковым вводом, который ждет, пока пользователь перестанет печатать, прежде чем выполнять:

function Search({ query, setQuery }) {
  const debouncedSearch = useCallback(
    debounce(q => {
      // Make your API call here
    }, 500),
    [],
  );

  useEffect(() => {
    debouncedSearch(query);
  }, [query, debouncedSearch]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

8. использовать эффект макета

useLayoutEffect имеет ту же сигнатуру, что и useEffect, но срабатывает синхронно после всех мутаций DOM. Используйте это для чтения макета из DOM и синхронного повторного рендеринга. Обновления, запланированные внутри useLayoutEffect, будут сброшены синхронно до того, как браузер успеет отрисовать.

Вариант использования - предотвратить скачки прокрутки:

function ScrollingList({ items, position }) {
  const ref = useRef(null);

  useLayoutEffect(() => {
    ref.current.scrollTop = position;
  }, [position]); // Only runs if position changes

  return (
    <div ref={ref}>
      {items.map(item => <div key={item.id}>{item.name}</div>)}
    </div>
  );
}

Компонент ScrollingList восстанавливает свою позицию прокрутки сразу после обновления DOM, но до того, как он был отрисован, предотвращая видимый скачок.

Создание пользовательских хуков

Пользовательские хуки — это, по сути, функции JavaScript, имя которых начинается с «использовать». Они могут вызывать другие хуки и даже другие пользовательские хуки.

1. использовать форму ввода

Давайте создадим собственный хук useFormInput, который управляет состоянием ввода формы.

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);
  
  function handleChange(e) {
    setValue(e.target.value);
  }

  return {
    value,
    onChange: handleChange
  };
}

Этот пользовательский хук сначала инициализирует переменную состояния `value` начальным значением. Затем он определяет функцию `handleChange()`, которая обновляет `value`, когда пользователь вводит в поле ввода. Наконец, он возвращает объект, содержащий `value` и `handleChange`. Теперь вы можете использовать этот хук в своих функциональных компонентах следующим образом:

function MyForm() {
  const name = useFormInput("");
  const password = useFormInput("");

  return (
    <form>
      <input type="text" {...name} placeholder="Name" />
      <input type="password" {...password} placeholder="Password" />
    </form>
  );
}

Здесь мы распределяем объект, возвращенный useFormInput, по элементам ввода. Это эквивалентно настройке value={name.value} и onChange={name.onChange}.

2. использовать локальное хранилище

Давайте взглянем на другой пользовательский хук: useLocalStorage. Этот хук позволяет вам читать и записывать в локальное хранилище так же, как вы управляете состоянием с помощью useState.

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(error);
      return initialValue;
    }
  });

  const setValue = value => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log(error);
    }
  };

  return [storedValue, setValue];
}

Этот пользовательский хук сначала пытается получить значение из локального хранилища, и если его там нет, он возвращает initialValue. Функция setValue используется для обновления как состояния, так и значения локального хранилища.

Вы можете использовать этот пользовательский хук точно так же, как useState:

function MyComponent() {
  const [name, setName] = useLocalStorage('name', 'Anonymous');

  return (
    <input 
      type="text"
      value={name}
      onChange={e => setName(e.target.value)}
    />
  );
}

В этом компоненте name хранится в локальном хранилище. Даже если вы обновите страницу, в поле ввода будет отображаться одно и то же значение благодаря возможностям локального хранилища.

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