High Order Components — фабрика крутых компонентов в React
Содержание
- Введение: что такое High Order Component
- Базовый пример HOC
- HOC для авторизации (проверка прав)
- HOC для загрузки данных
- HOC с конфигурацией (фабрика HOC)
- Композиция HOC (несколько обёрток)
- HOC для обработки ошибок
- Передача
refчерез HOC (React.forwardRef) - HOC для мемоизации (оптимизация)
- HOC vs Render Props vs Хуки
- Частые ошибки новичков
- Задание для закрепления (на 1–2 часа)
- Что дальше?
Цель урока: понять паттерн Higher Order Components (HOC) — функцию, которая принимает компонент и возвращает новый компонент с дополнительной логикой. Научиться создавать HOC для авторизации, логирования, загрузки данных, обработки ошибок и других сквозных задач.
Введение: что такое High Order Component
HOC (Higher Order Component) — это функция, которая принимает компонент и возвращает новый компонент с добавленной функциональностью.
Главная мысль: HOC — это как «обёртка» для компонента. Ты берёшь обычный компонент, кладёшь его в HOC, и на выходе получаешь компонент с новыми возможностями.
Почему HOC?
- Переиспользование логики — одна и та же логика для многих компонентов
- Сквозные задачи (cross-cutting concerns) — авторизация, логирование, загрузка
- Разделение ответственности — компонент занимается интерфейсом, HOC — логикой
- Альтернатива Render Props (в некоторых случаях)
Важно: HOC — это не часть React API, это паттерн, основанный на возможностях React.
Базовый пример HOC
Проблема: нужно добавить логирование рендеров во все компоненты.
Решение через HOC:
// HOC — добавляет логирование
function withLogger(WrappedComponent) {
// Возвращаем новый компонент
return function WithLogger(props) {
console.log(`Компонент ${WrappedComponent.name} рендерится`);
console.log('Пропсы:', props);
// Рендерим оригинальный компонент с его пропсами
return <WrappedComponent {...props} />;
};
}
// Обычный компонент
function UserProfile({ name, age }) {
return (
<div>
<h2>{name}</h2>
<p>Возраст: {age}</p>
</div>
);
}
// Оборачиваем компонент в HOC
const UserProfileWithLogger = withLogger(UserProfile);
// Использование
function App() {
return <UserProfileWithLogger name="Алексей" age={25} />;
}
// В консоли:
// Компонент UserProfile рендерится
// Пропсы: { name: "Алексей", age: 25 }
Что здесь происходит:
withLogger— функция, принимающая компонент- Возвращает новый компонент
WithLogger WithLoggerрендерит оригинальный компонент с теми же пропсами- Добавляет логирование перед рендером
HOC для авторизации (проверка прав)
// HOC для проверки авторизации
function withAuth(WrappedComponent) {
return function WithAuth(props) {
const isAuthenticated = localStorage.getItem('token') !== null;
if (!isAuthenticated) {
return <p>❌ Пожалуйста, войдите в систему</p>;
}
// Если авторизован — показываем компонент
return <WrappedComponent {...props} />;
};
}
// Компонент, требующий авторизации
function Dashboard({ user }) {
return (
<div>
<h1>Панель управления</h1>
<p>Добро пожаловать, {user.name}!</p>
</div>
);
}
// Оборачиваем
const ProtectedDashboard = withAuth(Dashboard);
function App() {
return <ProtectedDashboard user={{ name: 'Алексей' }} />;
}
HOC для загрузки данных
// HOC для загрузки данных с сервера
function withData(WrappedComponent, url) {
return function WithData(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Ошибка загрузки');
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
if (loading) return <p>Загрузка...</p>;
if (error) return <p>Ошибка: {error}</p>;
// Передаём данные в компонент через пропс data
return <WrappedComponent {...props} data={data} />;
};
}
// Компонент для отображения пользователей
function UserList({ data, title }) {
return (
<div>
<h2>{title}</h2>
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
// Оборачиваем
const UserListWithData = withData(
UserList,
'https://api.example.com/users'
);
function App() {
return <UserListWithData title="Список пользователей" />;
}
Оживи свой сайт. Освой JavaScript!
Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.
От теории — к реальным скриптам в твоём портфолио.
HOC с конфигурацией (фабрика HOC)
// Фабрика HOC с параметрами
function withStyles(styles) {
return function withStylesHOC(WrappedComponent) {
return function WithStyles(props) {
const style = {
border: '2px solid ' + styles.borderColor,
borderRadius: '8px',
padding: '16px',
background: styles.background || 'white',
margin: '10px 0'
};
return (
<div style={style}>
<WrappedComponent {...props} />
</div>
);
};
};
}
// Создаём HOC с разными стилями
const withRedBorder = withStyles({ borderColor: 'red', background: '#fff0f0' });
const withBlueBorder = withStyles({ borderColor: 'blue', background: '#f0f4ff' });
const withGreenBorder = withStyles({ borderColor: 'green', background: '#f0fff0' });
// Использование
function SimpleCard({ title, content }) {
return (
<div>
<h3>{title}</h3>
<p>{content}</p>
</div>
);
}
const RedCard = withRedBorder(SimpleCard);
const BlueCard = withBlueBorder(SimpleCard);
const GreenCard = withGreenBorder(SimpleCard);
function App() {
return (
<div>
<RedCard title="Ошибка" content="Что-то пошло не так" />
<BlueCard title="Информация" content="Обновление доступно" />
<GreenCard title="Успех" content="Операция выполнена" />
</div>
);
}
Композиция HOC (несколько обёрток)
Можно применять несколько HOC к одному компоненту:
// HOC для логирования
function withLogger(WrappedComponent) {
return function WithLogger(props) {
console.log(`Рендер ${WrappedComponent.name}`);
return <WrappedComponent {...props} />;
};
}
// HOC для авторизации
function withAuth(WrappedComponent) {
return function WithAuth(props) {
const isAuthenticated = localStorage.getItem('token') !== null;
if (!isAuthenticated) {
return <p>Требуется авторизация</p>;
}
return <WrappedComponent {...props} />;
};
}
// HOC для загрузки
function withLoading(WrappedComponent) {
return function WithLoading(props) {
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => setLoading(false), 1000);
}, []);
if (loading) return <p>Загрузка...</p>;
return <WrappedComponent {...props} />;
};
}
// Базовый компонент
function UserProfile({ name, age }) {
return (
<div>
<h2>{name}</h2>
<p>Возраст: {age}</p>
</div>
);
}
// Композиция HOC
const EnhancedUserProfile = withLogger(
withAuth(
withLoading(UserProfile)
)
);
// Или с помощью композиции функций
const compose = (...fns) => (component) =>
fns.reduceRight((acc, fn) => fn(acc), component);
const EnhancedUserProfile2 = compose(
withLogger,
withAuth,
withLoading
)(UserProfile);
function App() {
return <EnhancedUserProfile name="Алексей" age={25} />;
}
HOC для обработки ошибок
// HOC для обработки ошибок
function withErrorBoundary(WrappedComponent, fallbackComponent) {
return class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Ошибка в компоненте:', error, errorInfo);
}
render() {
if (this.state.hasError) {
if (fallbackComponent) {
return fallbackComponent({ error: this.state.error });
}
return (
<div style={{ color: 'red', padding: '20px' }}>
<h3>Что-то пошло не так</h3>
<p>{this.state.error?.message}</p>
</div>
);
}
return <WrappedComponent {...this.props} />;
}
};
}
// Использование
function BuggyComponent({ shouldCrash }) {
if (shouldCrash) {
throw new Error('Компонент упал!');
}
return <p>Компонент работает нормально</p>;
}
const SafeComponent = withErrorBoundary(
BuggyComponent,
({ error }) => (
<div style={{ color: 'orange' }}>
⚠️ Компонент временно недоступен: {error.message}
</div>
)
);
function App() {
const [crash, setCrash] = useState(false);
return (
<div>
<button onClick={() => setCrash(true)}>
Сломать компонент
</button>
<SafeComponent shouldCrash={crash} />
</div>
);
}
Передача ref через HOC (React.forwardRef)
// HOC с forwardRef
function withFocus(WrappedComponent) {
function WithFocus(props, ref) {
const focus = () => {
if (ref) {
ref.current?.focus();
}
};
return (
<div>
<button onClick={focus}>Фокус на поле</button>
<WrappedComponent {...props} ref={ref} />
</div>
);
}
return React.forwardRef(WithFocus);
}
// Базовый компонент
const InputField = forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
// Оборачиваем
const FocusableInput = withFocus(InputField);
function App() {
const inputRef = useRef(null);
return (
<div>
<FocusableInput
ref={inputRef}
placeholder="Введите текст"
/>
</div>
);
}
HOC для мемоизации (оптимизация)
// HOC для мемоизации компонента
function withMemo(WrappedComponent) {
return React.memo(WrappedComponent);
}
// Использование
const UserCard = withMemo(({ user }) => {
console.log('Рендер UserCard');
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});
function App() {
const [count, setCount] = useState(0);
const user = { name: 'Алексей', email: 'alex@example.com' };
return (
<div>
<button onClick={() => setCount(count + 1)}>
Счёт: {count}
</button>
<UserCard user={user} />
</div>
);
}
// UserCard не перерендерится при изменении count
HOC vs Render Props vs Хуки
| Характеристика | HOC | Render Props | Хуки |
|---|---|---|---|
| Синтаксис | Функция обёртки | Проп-функция | Стандартные функции |
| Сложность | Средняя | Средняя | Низкая |
| Вложенность | Можно цепочкой | Проблема с «адским» рендером | Отличная композиция |
| Гибкость | Средняя | Высокая | Высокая |
| Современность | Устаревает | Устаревает | Актуально |
| Когда использовать | Старые проекты, библиотеки | Старые проекты | Новые проекты |
Частые ошибки новичков
| Ошибка | Почему | Исправление |
|---|---|---|
| Мутируют компонент в HOC | Нарушает иммутабельность | Создавай копию, а не мутируй |
| Не передают props | Потеря данных | Всегда передавай {...props} |
Не используют displayName |
Сложно отлаживать | Добавь displayName в HOC |
| Создают HOC внутри render | Пересоздаётся при каждом рендере | Создавай вне компонента |
| Слишком много HOC | «Ад» из обёрток | Комбинируй через compose |
Задание для закрепления (на 1–2 часа)
Создай три HOC:
withLoading— показывает индикатор загрузки (можно через пропisLoadingили внутреннее состояние с таймером)withPermission— проверяет права доступа: принимает список разрешений (permissions) и проверяет их перед рендеромwithTheme— добавляет тему (светлая/тёмная) через контекст и передаёт её в компонент через пропсtheme
Дополнительно: создай страницу, где используются все три HOC:
- Компонент
AdminPanelобёрнут вwithAuth - Компонент
UserListобёрнут вwithDataиwithLoading - Компонент
Dashboardобёрнут вwithThemeиwithPermissions
Проверка: убедись, что каждый HOC добавляет свою функциональность без изменения исходных компонентов.
Что дальше?
В следующем уроке разберём Portals — механизм рендеринга компонентов за пределами родительского DOM-дерева. Ты узнаешь, как создавать модальные окна, всплывающие подсказки и другие UI-элементы, которые должны появляться поверх всего контента, нарушая обычную иерархию React.