5 кастомных React хуков, которые улучшат ваш код

2 года назад·3 мин. на чтение

В этой статье рассмотрим несколько очень полезных React хуков.

1. Хук useWindowSize - хук для получения размера экрана

Я уверен, что в некоторых проектах, над которыми вы работали, вам нужно было получить ширину и высоту окна пользователя. Так вот, теперь у вас есть хук для этого, так что вы можете сделать это еще проще, чем раньше.
import { useState, useEffect } from "react";

interface WindowSize {
  width: number;
  height: number;
}

const useWindowSize = (): WindowSize => {
  const [windowSize, setWindowSize] = useState<WindowSize>({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };
    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return windowSize;
};
Этот хук может быть особенно полезен при реализации отзывчивого дизайна, когда по какой-то причине вам нужно запустить определенный код при определенных размерах. Более продвинутый вариант этого хука можно найти в статье “Хук useResize для отслеживания ширины экрана в ReactJS”.

2️. Хук useKeyPress - хук для определения нажатия клавиши

Следующий хук позволяет определить, когда нажата определенная клавиша. Это может вызвать события или действия, основанные на нажатой клавише. Например, для закрытия модального окна, отправки формы и т.д.
import { useState, useEffect } from "react";

const useKeyPress = (targetKey: string): boolean => {
  const [keyPressed, setKeyPressed] = useState(false);

  const downHandler = ({ key }: KeyboardEvent) => {
    if (key === targetKey) {
      setKeyPressed(true);
    }
  };

  const upHandler = ({ key }: KeyboardEvent) => {
    if (key === targetKey) {
      setKeyPressed(false);
    }
  };

  useEffect(() => {
    window.addEventListener("keydown", downHandler);
    window.addEventListener("keyup", upHandler);

    return () => {
      window.removeEventListener("keydown", downHandler);
      window.removeEventListener("keyup", upHandler);
    };
  }, []);

  return keyPressed;
};
Пример использования:
const closeModalKeyPress = useKeyPress("Escape");

3. Хук useInterval - хук для вызова функции setInterval

Этот хук позволяет вам использовать функцию setInterval в качестве хука. Как и функция setInterval, этот хук имеет множество применений, например, анимация, обновление данных через регулярные промежутки времени или даже установка таймера.
import { useState, useEffect, useRef } from "react";

const useInterval = (callback: () => void, delay: number | null) => {
  const savedCallback = useRef<() => void>();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    function tick() {
      savedCallback.current && savedCallback.current();
    }
    if (delay !== null && delay > 0) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    } else {
      tick();
    }
  }, [delay]);
};
Можно использовать этот хук следующим образом:
const [count, setCount] = useState(0);

useInterval(() => {
  setCount(count + 1);
}, 1000);

4. Хук useDebounce

Теперь мы поговорим о хуке, который выполняет функцию только после того, как пройдет определенное количество времени без ее вызова. Это полезно, например, для ограничения скорости вызовов API или обновления состояния при изменении ввода, например, когда вы набираете текст в поисковой строке.
import { useEffect, useRef } from 'react';

export function useDebounce(callback:Function, timeout: number = 200, deps: Array<any> = []) {
  const data = useRef({ firstTime: true });
  useEffect(() => {
    const { firstTime, clearFunc } = data.current;

    const handler = setTimeout(() => {
      if (clearFunc && typeof clearFunc === 'function') {
        clearFunc();
      }
      data.current.clearFunc = callback();
    }, timeout);

    return () => {
      clearTimeout(handler);
    };
  }, [timeout, ...deps]);
}

export default useDebounce;
Пример использования:
const [inputValue, setInputValue] = useState("");

useDebounce(() => {
  // вызов APIl
}, 500);

5️. Хук useThrottle

Throttle означает, что функция будет выполняться один раз за каждый заданный промежуток времени. Это может быть полезно для предотвращения быстрого запуска слишком большого количества вызовов API или событий.
import { useEffect, useRef } from 'react';

export const useThrottle = (callback:Function, delay: number = 200, deps: Array<any> = []) => {
  const lastRan = useRef(Date.now());

  useEffect(
    () => {
      const handler = setTimeout(function() {
        if (Date.now() - lastRan.current >= delay) {
          callback();
          lastRan.current = Date.now();
        }
      }, delay - (Date.now() - lastRan.current));

      return () => {
        clearTimeout(handler);
      };
    },
    [delay, ...deps],
  );
};

export default useThrottle;
Пример:
const [inputValue, setInputValue] = useState("");

useThrottle(() => {
  // вызов API
}, 500);
Еще больше крутых хуков вы можете найти в каталоге хуков.

Что нужно знать о Redux - action/dispatch/reducer/store

2 года назад·3 мин. на чтение

В этой статье рассмотрим работу с Redux - его основные понятия, и как Redux работает с данными.

Redux - это стэйт менеджер для JS приложений. Он может работать не только в React приложениях. Просто так сложилось исторически, что они часто упоминаются вместе. Redux хранит состояние в дереве объектов внутри единого стора. Единственная возможность изменить состояние - отправить action. Action это объект, который описывает действие. Он как бы отвечает на вопрос "Что я хочу изменить в состоянии?".
// types.js
const ADD_TODO = "ADD_TODO"
// actions.js
import { ADD_TODO } from "./types.js"

export const addTodoAction = {
  type: ADD_TODO,
  payload: {
     id: 1,
     text: "Изучить Redux",
     done: false
   }
}
Далее action попадает в reducer, где описано как состояние должно быть изменено ("Как я хочу изменить состояние?").
//  todo-reducer.js
import { ADD_TODO } from "./types.js"

export const todoReducer = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO: {
      const item = action.payload
      return [...state, item]
    }
    default:
      return state
  }
}

Для чего нужен Redux?

Redux и в целом любая другая система работы с состоянием нужна для контроля над состоянием. Веб приложения усложняются, добавляются новые фичи и контроль над приложением теряется. Становится сложно охватывать большой проект как единое целое. Становится cложно передавать изменения. Встроенными способами в React их приходилось бы передавать по цепочке от одного компонента в другой или через Context API. По дороге обновленное состояние может также влиять на другие компоненты и т.д. И такая бесконтрольность расползается по всему проекту и может послужить появлению новых багов и добавит сложность в отладке. Теряется прозрачность того что происходит.
Action’ы добавляют порядок в происходящие изменения. Action - это объект, и его можно залогировать и последовательно отслеживать как меняется состояние. Подытожив, можно вывести три принципа Redux:
  • Единственный источник правды - единый стор, который меняются и всегда содержит актуальные данные.
  • Состояние - только для чтения. Нельзя менять его напрямую - только через action’ы.
  • Изменения делаются только при помощи чистых функций.
  • Эти функции (редьюсеры) принимают в качестве аргумента старое состояние и возвращают обновленное состояние. И всегда при одних и тех же данных результат этих функций будет одинаков.

Понятия в Redux

Action

Action (экшн) - это источник данных в стор, Action включает тип и некоторую информацию (payload). Тип обычно имеет строковое значение. Он нужен чтобы reducer понимал какой action был отправлен. Далее к стору будет применен payload.

Reducer

Reducer (редьюсер) - это функция, которая определяет как должно меняться состояние в зависимости от аction’а. Редьюсеры должны быть чистым функциями - они должны вычислить следующее состояние и вернуть новый объект состояния. Никаких сайд эффектов, мутаций состояния, и вызовов API в редьюсере быть не должно.

Store

Store - объединяет аction’ы и редьюсеры
  • Хранит состояние приложения.
  • Предоставляет доступ к состоянию через getState.
  • Позволяет изменять состояние через dispatch.
  • Регистрирует подписчиков через subscribe.
Теперь когда мы знаем основные понятия сформулируем флоу - движение данных в Redux. Сначала вызываем функцию dispatch, передав в нее action.
// App.jsx
import { useDispatch } from "react-redux"
import { ADD_TODO } from "./types.js"

export default App() {
  const dispatch = useDispatch()
 
  const handleTodoAdd = () => {
    dispatch({
      type: ADD_TODO,
       payload: {
         id: 1,
         text: "Изучить Redux",
         done: false
       },
    })
  }

  return (
    <button type="button" onClick={handleTodoAdd}>Добавить todo</button>
  )
}
Далее Redux вызывает переданный в него редьюсер. В редьюсер отправляется два аргумента - текущее состояние (state) и action.
//  todo-reducer.js
import { ADD_TODO } from "./types.js"

export const todoReducer = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO: {
      const item = action.payload
      return [...state, item]
    }
    default:
      return state
  }
}
Redux сохраняет весь объект, который вернулся из корневого редьюсера. Почему редьюсер - корневой? В Redux передается один редьюсер, но его можно разделить на несколько и собрать вместе при помощи combineReducers. Результатом этой функции будет корневой редьюсер. Обычно это делается, когда в приложении есть несколько модулей и есть смысл разделить большой редьюсер на несколько маленьких. Но для мелких приложений это совсем не обязательно.
// reducers.js
import { combineReducers } from "redux"
import { todoReducer } from "./todo-reducer"

export const rootReducer = combineReducers({
  todos: todoReducer,
})
Как уже упоминалось, Redux работает не только с ReactJS. Это универсальный инструмент. Однако для того чтобы использовать его в ReactJS нужен специальный байндинг, который ставится как отдельный пакет - react-redux.

Как подключить Redux к ReactJS проекту

Установить Redux с помощью команды.
npm i redux react-redux
Проинициализировать store и обернуть приложение тегом <Provider> c переданным в него объектом store.
// index.js
import React from "react"
import ReactDOM from "react-dom/client"

import { createStore } from "redux"
import { Provider } from "react-redux"

import App from "./App"
import { rootReducer } from "./reducers"

const store = createStore(rootReducer)

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);