Предисловие
Однажды мне понадобилось написать гибкий на типизацию компонент в React. Мне нужно было, чтобы в зависимости от одного пропса в виде массива элементов, менялся другой пропс, а точнее аргумент рендер функции, которую я тоже должен передавать в качестве пропса. В обычных функциях тс, я бы подобную проблему решил через дженерики, и поэтому у меня появилась идея написать компонент дженерик.
Базовые понятия
Прежде чем написать свой первый компонент-дженерик, нужно понять и научиться писать обычные Тайпскрипт Дженерики.
Для чего вообще нужны дженерики? Они нужны для того, чтобы мы могли использовать наши конструкции кода не только с заранее заданными типами, но и иметь вариативность.
Проще написать, к сожалению, не получилось, поэтому предлагаю разобраться на примере.
Давайте представим, что у нас есть какая-то функция, которая имитирует запрос к серверу и отдает нам значение в виде обертки над нашим аргументом.
function request(arg: string) {
return {
status: 200,
data: arg
}
}
По коду выше наша функция будет иметь тип
function request(arg: string): {
status: number;
data: string;
}
Все, что написано выше - это, конечно, здорово, но давайте представим, что мы хотим работать с этой фукнцией не только с аргументом строкой.
Есть 3 варианта...
Использовать тип unknown или any
interface Response {
status: number;
data: unknown;
}
function request(arg: unknown): Response {
return {
status: 200,
data: arg
}
}
Написать еще одну функцию которая работает с числами
function request(arg: number) {
return {
status: 200,
data: arg
}
}
И конечно же использовать дженерик
function request<T>(arg: T) {
return {
status: 200,
data: arg
}
}
Что нам даст использование дженерика
function request<T>(arg: T) {
return {
status: 200,
data: arg
}
}
// Если мы вызовем request с числом
request(100)
// То request будет иметь тип
function request<number>(arg: number): {
status: number;
data: number;
}
// А если со строкой
request('100');
// То
function request<string>(arg: string): {
status: number;
data: string;
}
Если хотите более подробно познакомиться с дженериками, можете посмотреть документацию
Как пишутся компоненты-дженерики
Классовые:
class CustomComponent<T> extends React.Component<T> {
// ...
}
Функциональные:
function CustomComponent<T> (props: React.PropsWithChildren<T>): React.ReactElement{
// ...
}
const CustomComponent = <T, >(props: React.PropsWithChildren<T>: React.ReactElement => {
// ...
}
Какие задачи решают
Давайте рассмотрим полезность на простом примере
interface CustomComponentProps<T> {
data: T[]
onClick: (element: T) => void;
}
class CustomComponent<T> extends React.Component<CustomComponentProps<T>> {
render() {
return (
<div>
<h1>Список</h1>
<ul>
{
this.props.data.map((element) => (
<li onClick={() => this.props.onClick(element)}></li>
))
}
</ul>
</div>
)
}
}
Что же нам даст использование этого компонента на деле
const AnotherCustomComponent: React.FC = () => {
const data = ['text', 'text', 'text'];
return (
<div>
<CustomComponent data={data} onClick={/* */} />
</div>
)
}
в примере выше CustomComponent будет ожидать в onClick функцию (element: string) => void
так как в data был передан массив строк
Теперь давайте рассмотрим пример, где в качестве data у нас будет не массив примитивов, а массив сущностей
class User {
constructor(
public name: string,
public lastName: string
){}
get fullName() {
return `${this.name} ${this.lastName}`
}
}
const AnotherCustomComponent: React.FC = () => {
const data = [
new User('Джон', 'Сина'),
new User('Дуэйн', 'Джонсон'),
new User('Дейв', 'Батиста'),
];
return (
<div>
{/* в он клик нам не придется ручками подставлять тип.
Как в примере выше, он сам выведется из того, что было передано в data */}
<CustomComponent data={data} onClick={(user) => alert(user.fullName)} />
</div>
)
}
теперь функция onClick будет иметь тип (element: User) => void
Давайте рассмотрим немного другой пример
interface CustomComponentProps<T> {
data: T[]
onClick: (element: T) => void;
// делаем children рендер функцией для элемента списка
children: (element: T) => React.ReactElement
}
class CustomComponent<T> extends React.Component<CustomComponentProps<T>> {
render() {
return (
<div>
<h1>Список</h1>
<ul>
{
this.props.data.map((element) => (
<li onClick={() => this.props.onClick(element)}>{this.props.children(element)}</li>
))
}
</ul>
</div>
)
}
}
class User {
constructor(
public name: string,
public lastName: string
){}
get fullName() {
return `${this.name} ${this.lastName}`
}
}
const AnotherCustomComponent: React.FC = () => {
const data = [
new User('Джон', 'Сина'),
new User('Дуэйн', 'Джонсон'),
new User('Дейв', 'Батиста'),
];
return (
<div>
<CustomComponent data={data} onClick={(user) => alert(user)}>
{
// тут тип как в onClick тоже высчитается
(user) => {
return <div>Пользователь: {user.fullName}</div>
}
}
</CustomComponent>
</div>
)
}
Так как мы в data передавали массив юзеров children будет иметь тип (element: User) => React.ReactElement
По аналогии с дженериками-функциями в дженерики-компоненты можно явно определить нужный тип, с которым мы бы хотели работать, но такой синтаксис я редко использую
Пример валидного кода:
const AnotherCustomComponent: React.FC = () => {
const data = [
new User('Джон', 'Сина'),
new User('Дуэйн', 'Джонсон'),
new User('Дейв', 'Батиста'),
];
return (
<div>
{/* тут мы явно задаем что мы хотим работать с юзером */}
<CustomComponent<User>
data={data}
onClick={(user) => alert(user)}
>
{
// тут тип как в onClick тоже высчитается
(user) => {
return <div>Пользователь: {user.fullName}</div>
}
}
</CustomComponent>
</div>
)
}
Пример не валидного кода:
const AnotherCustomComponent: React.FC = () => {
const data = [
new User('Джон', 'Сина'),
new User('Дуэйн', 'Джонсон'),
new User('Дейв', 'Батиста'),
];
return (
<div>
{/* тут мы явно задаем что мы хотим работать со строкой
и при передаче в data массив юзеров будет ошибка */}
<CustomComponent<string>
data={data}
onClick={(user) => alert(user)}
>
{
(user) => {
return <div>Пользователь: {user.fullName}</div>
}
}
</CustomComponent>
</div>
)
}
Полезность на личном опыте
Реальные примеры из жизни не стал прикладывать, так как мои компоненты довольно жирненькие, поэтому решил показать полезность на псевдо примерах, но в качестве бонуса решил словами описать последнюю проблему, которую решил через компонент-дженерик.
Мне нужно было сделать списки для товаров и категорий, которые можно было драг-н-дропать, для изменения порядка отображения, и функционал у этих двух списков идентичен, кроме отображения. Я написал один компонент, который мог работать с драг-н-дропом, у него был пропс children который являлся рендер функцией для элемента, что мне и позволило менять отображение списка при одинаковом функционале.
Заключение
В заключении хотелось бы сказать, что компоненты-дженерики хоть и довольно нишевы, но бывают очень полезны. Надеюсь моя статья будет очень полезна и поможет вам написать свой первый компонент-дженерик.
faiwer
Главная проблема это HoC-и. Они теряют все generic-и, т.к. в TS нет High Order Kinds. И приходится cast-ить типы руками. А так штука хорошая.