001 Что такое HOC
HOC - High order component - это компонент, который принимает в себя компонент и возвращает новый компонент
Простой HOC
принимает в себя в качестве пропса другой компонент и возвращает из себя модифицированный компонент (по желанию с пропсами)
И примерно так выглядит HOC
с использованием типов
Важно!
- Менять сам компонент нельзя
- Всегда нужно передавать
props
внутрь оборачиваемого компонента- Необходимо для отладки задавать
displayName
- Нельзя использовать
HOC
внутри отрисовки
002 Layout
Определимся с тем, что у нас есть макете:
- Боковая панель (которая скрывается на мобильной версии сайта)
- основная контентная панель
- Футер
- Хедер (который показывается на мобилке)
Чтобы поддерживать одинаковый макет на всех страницах, мы можем создать отдальный компонент, который будет определять, как уже будет выглядеть наш макет. Конкретно: он отобразит, как элементы должны будут располагаться на странице.
Элементы лейаута можно расположить в отдельной папке, а не складировать вместе с компонентами
Так выглядят компоненты, которые будут располагаться на странице:
- Футер
- Хедер
- Сайдбар
Отдельно для них были сделаны стили, которые впоследствии уже определят расположение элементов на странице
/// Footer props
import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
export interface IFooterProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {}
/// Footer .tsx
import React from 'react';
import styles from 'Header.module.css';
import cn from 'classnames';
import { IFooterProps } from './Footer.props';
export const Footer = ({ ...props }: IFooterProps) => {
return <div {...props}>Footer</div>;
};
/// Header props
import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
export interface IHeaderProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {}
/// Header .tsx
import React from 'react';
import styles from 'Header.module.css';
import cn from 'classnames';
import { IHeaderProps } from './Header.props';
export const Header = ({ ...props }: IHeaderProps) => {
return <div {...props}>Header</div>;
};
/// Sidebar props
import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
export interface ISidebarProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {}
/// Sidebar .tsx
import React from 'react';
import styles from 'Sidebar.module.css';
import cn from 'classnames';
import { ISidebarProps } from './Sidebar.props';
export const Sidebar = ({ ...props }: ISidebarProps) => {
return <div {...props}>Sidebar</div>;
};
А уже так выглядит сам Layout
:
Экстендить данные пропсы див-элементами мы не будем, чтобы сохранять относительную прозрачность данного компонента
Layout.props.ts
import { ReactNode } from 'react';
export interface ILayoutProps {
children: ReactNode;
}
Конкретно тут в качестве параметра children
будут приниматься все компоненты и вставляться в div
Layout.tsx
import React from 'react';
import styles from 'Layout.module.css';
import cn from 'classnames';
import { ILayoutProps } from './Layout.props';
import { Header } from './Header/Header';
import { Footer } from './Footer/Footer';
import { Sidebar } from './Sidebar/Sidebar';
export const Layout = ({ children }: ILayoutProps) => {
return (
<>
<Header />
<div>
<Sidebar />
<div>{children}</div>
</div>
<Footer />
</>
);
};
И теперь мы можем обернуть наш основной код в Layout
компонент, который принимает в себя все остальные компоненты системы
index.tsx
export default function Home(): JSX.Element {
const [rating, setRating] = useState<number>(4);
return (
<Layout>
<Htag tag='h1'>Заголовок</Htag>
<Button appearance='primary' arrow='right'>
Кнопка
</Button>
<Button appearance='ghost' arrow='down'>
Кнопка
</Button>
<P size='l'>Большой</P>
<P>Средний</P>
<P size='s'>Маленький</P>
<Tag size='s'>Ghost</Tag>
<Tag size='m' color='red'>
Red
</Tag>
<Tag size='s' color='green'>
Green
</Tag>
<Tag color='primary'>Green</Tag>
<Rating rating={rating} isEditable setRating={setRating} />
</Layout>
);
}
003 Пишем HOC withLayout
Теперь уже будет писаться сам HOC
компонент, который будет дополнять наш макет дополнительным функционалом.
Конкретно сейчас будем экспортировать компонент withLayout
, который в себя принимает другой компонент Component
и рендерит его внутри Layout
Layout.tsx
import React, { FunctionComponent } from 'react';
import styles from 'Layout.module.css';
import cn from 'classnames';
import { ILayoutProps } from './Layout.props';
import { Header } from './Header/Header';
import { Footer } from './Footer/Footer';
import { Sidebar } from './Sidebar/Sidebar';
const Layout = ({ children }: ILayoutProps) => {
return (
<>
<Header />
<div>
<Sidebar />
<div>{children}</div>
</div>
<Footer />
</>
);
};
export const withLayout = <T extends Record<string, unknown>>(Component: FunctionComponent<T>) => {
return function withLayoutComponent(props: T): JSX.Element {
return (
<Layout>
<Component {...props} />
</Layout>
);
};
};
Как было:
Мы оборачивали все внутренности компонента Home
внутрь Layout
Тут изначально мы экспортируем дефолтно нашу функцию, которая рендерится первой (ввиду того, что она индексная).
pages > index.tsx
import React, { useState } from 'react';
import { Button, Htag, P, Rating, Tag } from '../components';
import { Layout } from '../layout/Layout';
export default function Home(): JSX.Element {
const [rating, setRating] = useState<number>(4);
return (
<Layout>
<Htag tag='h1'>Заголовок</Htag>
<Button appearance='primary' arrow='right'>
Кнопка
</Button>
<Button appearance='ghost' arrow='down'>
Кнопка
</Button>
<P size='l'>Большой</P>
<P>Средний</P>
<P size='s'>Маленький</P>
<Tag size='s'>Ghost</Tag>
<Tag size='m' color='red'>
Red
</Tag>
<Tag size='s' color='green'>
Green
</Tag>
<Tag color='primary'>Green</Tag>
<Rating rating={rating} isEditable setRating={setRating} />
</Layout>
);
}
Как стало:
Сейчас мы рендерим дефолтно нашу функцию, которая была вложена в другой HOC
-компонент.
Тут мы вкладываем сам компонент в HOC
, а не его внутренности в другой такой же компонент (не в обычный функциональный, а в HOC
)
import React, { useState } from 'react';
import { Button, Htag, P, Rating, Tag } from '../components';
import { withLayout } from '../layout/Layout';
function Home(): JSX.Element {
const [rating, setRating] = useState<number>(4);
return (
<>
<Htag tag='h1'>Заголовок</Htag>
<Button appearance='primary' arrow='right'>
Кнопка
</Button>
<Button appearance='ghost' arrow='down'>
Кнопка
</Button>
<P size='l'>Большой</P>
<P>Средний</P>
<P size='s'>Маленький</P>
<Tag size='s'>Ghost</Tag>
<Tag size='m' color='red'>
Red
</Tag>
<Tag size='s' color='green'>
Green
</Tag>
<Tag color='primary'>Green</Tag>
<Rating rating={rating} isEditable setRating={setRating} />
</>
);
}
export default withLayout(Home);