Реакт Компоненты-дженерики

—

от автора

Предисловие

Однажды мне понадобилось написать гибкий на типизацию компонент в 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 который являлся рендер функцией для элемента, что мне и позволило менять отображение списка при одинаковом функционале.

Ссылка на gist

Заключение

В заключении хотелось бы сказать, что компоненты-дженерики хоть и довольно нишевы, но бывают очень полезны. Надеюсь моя статья будет очень полезна и поможет вам написать свой первый компонент-дженерик.


ссылка на оригинал статьи https://habr.com/ru/articles/576646/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *