| title | Занятие 32 |
|---|---|
| description | Переиспользование кода с кастомными хуками |
Проблема дублирования логики
// ComponentOne.jsx
const [isHovered, setIsHovered] = useState(false);
const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
...
</div>;// ComponentTwo.jsx
const [isHovered, setIsHovered] = useState(false);
const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
...
</div>;Логика полностью идентична. Как ее переиспользовать?
Раньше для этого использовали:
- HOC (Higher-Order Components) - Компоненты высшего порядка.
- Render Props.
HOC (Higher-Order Components)
- Функция, которая принимает компонент и возвращает новый компонент
- Позволяет добавлять логику "снаружи"
- Используется для переиспользования поведения
function withHover(WrappedComponent) {
return function (props) {
const [isHovered, setIsHovered] = useState(false);
const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
return (
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<WrappedComponent {...props} isHovered={isHovered} />
</div>
);
};
}
function MyComponent({ isHovered }) {
return <div>{isHovered ? "На меня навели курсор!" : "Наведите курсор"}</div>;
}
export default withHover(MyComponent);Плюсы:
- Разделение обязанностей (UI отдельно, логика отдельно)
- Легко оборачивать несколько компонентов
Минусы:
- "Wrapper hell" (дерево компонентов сильно усложняется)
- Возможны конфликты props
- Логика скрыта внутри обёртки
Render Props
- Компонент принимает функцию как child
- Эта функция управляет тем, что будет отрисовано
- Позволяет гибко делиться состоянием и поведением
function Hover({ children }) {
const [isHovered, setIsHovered] = useState(false);
const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
return (
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
{children(isHovered)}
</div>
);
}
function App() {
return (
<Hover>
{(isHovered) => (
<div>{isHovered ? "На меня навели курсор!" : "Наведите курсор"}</div>
)}
</Hover>
);
}Плюсы:
- Гибкость — родитель решает, как отрисовать
- Чёткий контроль над данными
Минусы:
- Синтаксис иногда "шумный"
- Легко создать "пирамиду функций"
Оба подхода рабочие, но имеют недостатки
Хуки предложили более элегантное решение.
Что такое кастомные хуки?
Кастомный хук — это JavaScript-функция, имя которой начинается с use, и которая может вызывать другие хуки.
Это не часть React API. Это соглашение, которое позволяет извлекать логику компонента в переиспользуемые функции.
Зачем нужны кастомные хуки?
- Убираем дублирование логики
- Разделяем бизнес-логику и UI
- Повышаем читаемость и тестируемость
Правила хуков все еще действуют!
Вызывайте хуки только на верхнем уровне вашей React-функции.
Вызывайте хуки только из React-компонентов или из других кастомных хуков.
хук useToggle
Компонент с логикой внутри:
import { useState } from "react";
function ToggleComponent() {
const [isOn, setIsOn] = useState(false);
const toggle = () => setIsOn((prevIsOn) => !prevIsOn);
return <button onClick={toggle}>{isOn ? "Включено" : "Выключено"}</button>;
}Извлекаем логику в хук:
import { useState, useCallback } from "react";
export function useToggle(initialState = false) {
const [state, setState] = useState(initialState);
const toggle = useCallback(() => setState((prevState) => !prevState), []);
return [state, toggle];
}Используем наш новый хук:
import { useToggle } from "../hooks/useToggle";
function ToggleComponent() {
const [isOn, toggle] = useToggle(false);
return <button onClick={toggle}>{isOn ? "Включено" : "Выключено"}</button>;
}Теперь useToggle можно использовать в любом компоненте!
хук useRequest
function UserProfile() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("[https://api.example.com/user/1](https://api.example.com/user/1)")
.then((res) => res.json())
.then((data) => setUser(data))
.catch((err) => setError(err))
.finally(() => setIsLoading(false));
}, []);
// ... рендер состояний
}Создаем хук useRequest
import { useState, useEffect } from "react";
export function useRequest(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!url) return;
setIsLoading(true);
fetch(url)
.then((res) => res.json())
.then((data) => setData(data))
.catch((err) => setError(err))
.finally(() => setIsLoading(false));
}, [url]);
return { data, isLoading, error };
}Используем useRequest в компоненте:
import { useRequest } from "../hooks/useRequest";
function UserProfile({ userId }) {
const {
data: user,
isLoading,
error,
} = useRequest(`https://api.example.com/user/${userId}`);
if (isLoading) return <div>Загрузка...</div>;
if (error) return <div>Ошибка: {error.message}</div>;
return <div>Имя пользователя: {user?.name}</div>;
}хук useInput
function useInput(initial = "") {
const [value, setValue] = useState(initial);
const onChange = (e) => setValue(e.target.value);
return { value, onChange };
}function MyForm() {
const props = useInput();
return <input {...props} placeholder="Введите имя" />;
}хук useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(
() => JSON.parse(localStorage.getItem(key)) ?? initialValue
);
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}Лучшие практики
- Имя должно начинаться с use
- Фокус на одной задаче. Не делайте один хук, который делает всё. Лучше несколько маленьких, которые можно комбинировать.
- Хук — чистая функция (без сайд-эффектов вне React API)
- Логику можно комбинировать: хуки вызывают хуки
- Выносите только повторяющийся код
Дополнительные материалы