Компонент высшего порядка (HOC) — это функция, которая принимает компонент и возвращает новый компонент с расширенной функциональностью. Это паттерн композиции для переиспользования логики компонентов.
Базовый пример HOC:
// HOC для добавления логирования
const withLogging = (WrappedComponent) => {
return function WithLoggingComponent(props) {
useEffect(() => {
console.log('Component mounted:', WrappedComponent.name);
}, []);
return <WrappedComponent {...props} />;
};
};
// Использование
const Button = ({ onClick, children }) => (
<button onClick={onClick}>{children}</button>
);
const ButtonWithLogging = withLogging(Button);HOC — это продвинутый паттерн в React для переиспользования логики компонентов. Они не являются частью React API, а возникают из композиционной природы React.
// HOC принимает компонент
const withEnhancement = (WrappedComponent) => {
// Возвращает новый компонент
const EnhancedComponent = (props) => {
// Добавляет новую логику
const [state, setState] = useState();
// Рендерит оригинальный компонент с новыми пропсами
return (
<WrappedComponent
{...props}
enhancedProp={state}
/>
);
};
// Задаём displayName для отладки
EnhancedComponent.displayName =
`withEnhancement(${WrappedComponent.displayName || WrappedComponent.name})`;
return EnhancedComponent;
};const withAuth = (WrappedComponent) => {
return function WithAuthComponent(props) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Проверка аутентификации
checkAuth()
.then(authenticated => {
setIsAuthenticated(authenticated);
setIsLoading(false);
});
}, []);
if (isLoading) {
return <div>Загрузка...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <WrappedComponent {...props} />;
};
};
// Использование
const Dashboard = () => <div>Панель управления</div>;
const ProtectedDashboard = withAuth(Dashboard);const withDataFetching = (url) => (WrappedComponent) => {
return function WithDataFetchingComponent(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
return (
<WrappedComponent
{...props}
data={data}
loading={loading}
error={error}
/>
);
};
};
// Использование
const UserList = ({ data, loading, error }) => {
if (loading) return <div>Загрузка...</div>;
if (error) return <div>Ошибка: {error.message}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
const UsersWithData = withDataFetching('/api/users')(UserList);const withWindowSize = (WrappedComponent) => {
return function WithWindowSizeComponent(props) {
const [windowSize, setWindowSize] = useState({
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 (
<WrappedComponent
{...props}
windowSize={windowSize}
/>
);
};
};
// Использование
const ResponsiveComponent = ({ windowSize }) => (
<div>
Ширина: {windowSize.width}px
{windowSize.width < 768 ? ' (Мобильный)' : ' (Десктоп)'}
</div>
);
const ResponsiveWithSize = withWindowSize(ResponsiveComponent);// Несколько HOC подряд
const enhance = compose(
withAuth,
withWindowSize,
withLogging
);
const EnhancedComponent = enhance(BaseComponent);
// Или вручную
const EnhancedComponent =
withAuth(
withWindowSize(
withLogging(BaseComponent)
)
);const compose = (...hocs) => (Component) =>
hocs.reduceRight((acc, hoc) => hoc(acc), Component);
// Использование
const enhance = compose(
withRouter,
withAuth,
connect(mapStateToProps)
);
const EnhancedApp = enhance(App);const withTheme = (theme = 'light') => (WrappedComponent) => {
return function WithThemeComponent(props) {
const themeConfig = {
light: {
background: '#fff',
color: '#000'
},
dark: {
background: '#000',
color: '#fff'
}
};
return (
<div style={themeConfig[theme]}>
<WrappedComponent {...props} theme={theme} />
</div>
);
};
};
// Использование
const ThemedButton = withTheme('dark')(Button);const withRefForwarding = (WrappedComponent) => {
const WithRef = React.forwardRef((props, ref) => {
return <WrappedComponent {...props} forwardedRef={ref} />;
});
WithRef.displayName =
`withRefForwarding(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithRef;
};
// Компонент, использующий ref
const FancyInput = ({ forwardedRef, ...props }) => (
<input ref={forwardedRef} {...props} />
);
const ForwardedInput = withRefForwarding(FancyInput);
// Использование
const App = () => {
const inputRef = useRef();
return <ForwardedInput ref={inputRef} />;
};// ❌ Плохо: мутация компонента
const withBadEnhancement = (WrappedComponent) => {
WrappedComponent.prototype.componentDidUpdate = function() {
console.log('Updated!');
};
return WrappedComponent;
};
// ✅ Хорошо: композиция
const withGoodEnhancement = (WrappedComponent) => {
return class extends React.Component {
componentDidUpdate() {
console.log('Updated!');
}
render() {
return <WrappedComponent {...this.props} />;
}
};
};// ❌ Плохо: теряем пропсы
const withBadHOC = (WrappedComponent) => {
return function BadHOC({ someProp }) {
return <WrappedComponent someProp={someProp} />;
};
};
// ✅ Хорошо: передаём все пропсы
const withGoodHOC = (WrappedComponent) => {
return function GoodHOC(props) {
return <WrappedComponent {...props} />;
};
};// ❌ Плохо: HOC делает слишком много
const withEverything = (WrappedComponent) => {
return function WithEverything(props) {
// Аутентификация
// Загрузка данных
// Тема
// Логирование
// ...много логики
};
};
// ✅ Хорошо: отдельные HOC для каждой задачи
const enhance = compose(
withAuth,
withDataFetching('/api/data'),
withTheme('dark'),
withLogging
);// HOC
const withMouse = (Component) => (props) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
// ... логика
return <Component {...props} mouse={position} />;
};
// Render Props
const Mouse = ({ render }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
// ... логика
return render(position);
};
// Использование Render Props
<Mouse render={position => <Component mouse={position} />} />// HOC
const withWindowSize = (Component) => (props) => {
const [size, setSize] = useState(getWindowSize());
// ... логика
return <Component {...props} windowSize={size} />;
};
// Custom Hook
const useWindowSize = () => {
const [size, setSize] = useState(getWindowSize());
// ... логика
return size;
};
// Использование Hook
const Component = () => {
const windowSize = useWindowSize();
// ... использование windowSize
};✅ Подходит для:
❌ Не подходит когда:
// Установка displayName для DevTools
const withDebugInfo = (WrappedComponent) => {
const WithDebugInfo = (props) => {
useEffect(() => {
console.log(`${WrappedComponent.name} rendered with props:`, props);
});
return <WrappedComponent {...props} />;
};
// Важно для React DevTools
WithDebugInfo.displayName =
`withDebugInfo(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithDebugInfo;
};HOC остаются мощным инструментом для создания переиспользуемой логики, хотя в современном React часто предпочитают использовать хуки для решения тех же задач.
Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪