Полезные API React для создания гибких компонентов с помощью TypeScript
Опубликовано: 2022-03-10 Вы когда-нибудь использовали React.createElement
напрямую? Как насчет React.cloneElement
? React — это больше, чем просто преобразование вашего JSX в HTML. Гораздо больше, и чтобы помочь вам повысить уровень своих знаний о менее известных (но очень полезных) API, с которыми поставляется библиотека React. Мы рассмотрим некоторые из них и некоторые варианты их использования, которые могут значительно улучшить интеграцию и полезность ваших компонентов.
В этой статье мы рассмотрим несколько полезных API React, которые не так широко известны, но чрезвычайно полезны для веб-разработчиков. Читатели должны иметь опыт работы с синтаксисом React и JSX, знание Typescript полезно, но не обязательно. Читатели получат все, что им нужно знать, чтобы значительно улучшить компоненты React при их использовании в приложениях React.
React.cloneElement
Большинство разработчиков, возможно, никогда не слышали о cloneElement
или когда-либо использовали его. Он был представлен относительно недавно для замены устаревшей функции cloneWithProps
. cloneElement
клонирует элемент, а также позволяет объединять новые реквизиты с существующим элементом, изменяя их или переопределяя по своему усмотрению. Это открывает чрезвычайно мощные возможности для создания API мирового класса для функциональных компонентов. Взгляните на подпись.
function cloneElement( element, props?, ...children)
Вот сжатая версия Typescript:
function cloneElement( element: ReactElement, props?: HTMLAttributes, ...children: ReactNode[]): ReactElement
Вы можете взять элемент, изменить его, даже переопределить его дочерние элементы, а затем вернуть его как новый элемент. Взгляните на следующий пример. Допустим, мы хотим создать компонент ссылок TabBar . Это может выглядеть примерно так.
export interface ITabbarProps { links: {title: string, url: string}[] } export default function Tabbar(props: ITabbarProps) { return ( <> {props.links.map((e, i) => <a key={i} href={e.url}>{e.title}</a> )} </> ) }
TabBar — это список ссылок, но нам нужен способ определить две части данных: заголовок ссылки и URL-адрес. Итак, мы хотим, чтобы структура данных передавалась с этой информацией. Таким образом, наш разработчик сделал бы наш компонент таким.
function App() { return ( <Tabbar links={[ {title: 'First', url: '/first'}, {title: 'Second', url: '/second'}] } /> ) }
Это здорово, но что, если пользователь хочет отображать элементы button
a
элементов? Ну, мы могли бы добавить еще одно свойство, которое сообщает компоненту, какой тип элемента отображать.
Но вы можете видеть, как это быстро станет громоздким, нам нужно будет поддерживать все больше и больше свойств для обработки различных вариантов использования и крайних случаев для максимальной гибкости.
Вот лучший способ, используя React.cloneElement
.
Мы начнем с изменения нашего интерфейса, чтобы он ссылался на тип ReactNode
. Это общий тип, который включает в себя все, что React может отображать, обычно элементы JSX, но также могут быть строками и даже null
. Это полезно для обозначения того, что вы хотите принимать компоненты React или JSX в качестве встроенных аргументов.
export interface ITabbarProps { links: ReactNode[] }
Теперь мы просим пользователя дать нам несколько элементов React, и мы будем отображать их так, как мы хотим.
function Tabbar(props: ITabbarProps) { return ( <> {props.links.map((e, i) => e // simply return the element itself )} </> ) }
Это совершенно верно и отобразит наши элементы. Но мы забываем пару вещей. Во-первых, key
! Мы хотим добавить ключи, чтобы React мог эффективно отображать наши списки. Мы также хотим изменить наши элементы, чтобы сделать необходимые преобразования, чтобы они соответствовали нашему стилю, например className
и так далее.
Мы можем сделать это с помощью React.cloneElement
и другой функции React.isValidElement
для проверки соответствия аргумента тому, что мы ожидаем!
React.isValidElement
Эта функция возвращает true
, если элемент является допустимым элементом React и React может его отобразить. Вот пример изменения элементов из предыдущего примера.
function Tabbar(props: ITabbarProps) { return ( <> {props.links.map((e, i) => isValidElement(e) && cloneElement(e, {key: `${i}`, className: 'bold'}) )} </> ) }
Здесь мы добавляем key prop к каждому элементу, который мы передаем, и одновременно выделяем жирным шрифтом каждую ссылку! Теперь мы можем принимать произвольные элементы React в качестве реквизита, например:
function App() { return ( <Tabbar links={[ <a href='/first'>First</a>, <button type='button'>Second</button> ]} /> ) }
Мы можем переопределить любой реквизит, установленный для элемента, и легко принимать различные типы элементов, что делает наш компонент намного более гибким и простым в использовании.
“
Преимущество здесь в том, что если бы мы хотели установить собственный обработчик onClick
для нашей кнопки, мы могли бы это сделать. Принятие самих элементов React в качестве аргументов — это мощный способ придать гибкости дизайну вашего компонента.
Функция useState
Используйте крючки! useState
чрезвычайно полезен и является фантастическим API для быстрого добавления состояния в ваши компоненты, например:
const [myValue, setMyValue] = useState()
Из-за среды выполнения JavaScript у него могут быть некоторые сбои. Помните замыкания?
В некоторых ситуациях переменная может быть неправильным значением из-за контекста, в котором она находится, например, в циклах for или асинхронных событиях. Это из-за лексической области видимости. Когда создается новая функция, лексическая область видимости сохраняется. Поскольку новой функции нет, лексическая область действия newVal
не сохраняется, поэтому значение фактически разыменовывается к моменту его использования.
setTimeout(() => { setMyValue(newVal) // this will not work }, 1000)
Что вам нужно сделать, так это использовать сеттер как функцию. При создании новой функции ссылка на переменную сохраняется в лексической области видимости, а currentVal передается самим хуком React useState.
setTimeout(() => { setMyValue((currentVal) => { return newVal }) }, 1000)
Это обеспечит правильное обновление вашего значения, поскольку функция установки вызывается в правильном контексте. Здесь React вызывает вашу функцию в правильном контексте для обновления состояния React. Это также можно использовать в других ситуациях, когда полезно воздействовать на текущее значение, React вызывает вашу функцию с первым аргументом в качестве текущего значения.
Примечание . Для дополнительного чтения по теме асинхронности и замыканий я рекомендую прочитать «Отложенную инициализацию и обновления функций useState
» Кента К. Доддса.
Встроенные функции JSX
Вот демонстрация Codepen встроенной функции JSX:
Не совсем React API за каждое слово.
JSX поддерживает встроенные функции и может быть очень полезен для объявления простой логики со встроенными переменными, если он возвращает элемент JSX.
“
Вот пример:
function App() { return ( <> {(() => { const darkMode = isDarkMode() if (darkMode) { return ( <div className='dark-mode'></div> ) } else { return ( <div className='light-mode'></div> ) // we can declare JSX anywhere! } })()} // don't forget to call the function! </> ) }
Здесь мы объявляем код внутри JSX, мы можем запускать произвольный код, и все, что нам нужно сделать, это вернуть функцию JSX для рендеринга.
Мы можем сделать это условным или просто выполнить некоторую логику. Обратите внимание на круглые скобки, окружающие встроенную функцию. Кроме того, особенно здесь, где мы вызываем эту функцию, мы могли бы даже передать аргумент в эту функцию из окружающего контекста, если бы захотели!
})()}
Это может быть полезно в ситуациях, когда вы хотите воздействовать на структуру данных коллекции более сложным образом, чем позволяет стандартный .map
внутри элемента JSX.
function App() { return ( <> {(() => { let str = '' for (let i = 0; i < 10; i++) { str += i } return (<p>{str}</p>) })()} </> ) }
Здесь мы можем запустить некоторый код для перебора набора чисел, а затем отобразить их в строке. Если вы используете генератор статических сайтов, такой как Gatsby, этот шаг также будет предварительно рассчитан.
component extends type
Чрезвычайно полезная для создания компонентов, поддерживающих автозаполнение, эта функция позволяет создавать компоненты, расширяющие существующие HTMLElements
или другие компоненты. В основном полезно для правильного ввода интерфейса элементов в Typescript, но фактическое приложение для JavaScript такое же.
Вот простой пример, предположим, что мы хотим переопределить одно или два свойства элемента button
, но при этом дать разработчикам возможность добавлять к кнопке другие свойства. Например, установка type='button'
или type='submit'
. Мы, очевидно, не хотим воссоздавать весь элемент кнопки, мы просто хотим расширить его существующие свойства и, возможно, добавить еще один реквизит.
import React, { ButtonHTMLAttributes } from 'react'
Сначала мы импортируем React и класс ButtonHTMLAttributes
, тип, который включает HTMLButtonElement
. Вы можете прочитать исходный код для этого типа интерфейса здесь:
И вы можете видеть, что команда React повторно реализовала все веб-API в TypeScript, чтобы их можно было проверить.
Затем мы объявляем наш интерфейс таким образом, добавляя наше свойство status
.
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { status?: 'primary' | 'info' | 'danger' }
И, наконец, мы делаем пару вещей. Мы используем деструктурирование ES6, чтобы извлечь нужные нам реквизиты ( status
и children
элементы) и объявить любые другие свойства как rest
. И в нашем выводе JSX мы возвращаем элемент кнопки со структурой ES6, чтобы добавить любые дополнительные свойства к этому элементу.
function Button(props: ButtonProps) { const { status, children, ...rest } = props // rest has any other props return ( <button className={`${status}`} {...rest} // we pass the rest of the props back into the element > {children} </button> ) }
Итак, теперь разработчик может добавить свойство type
или любое другое свойство, которое обычно имеет кнопка. Мы предоставили дополнительную поддержку, которую мы использовали в className
для установки стиля кнопки.
Вот весь пример:
import React, { ButtonHTMLAttributes } from 'react' export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { status?: 'primary' | 'info' | 'danger' } export default function Button(props: ButtonProps) { const { status, children, ...rest } = props return ( <button className={`${status}`} {...rest} > {children} </button> ) }
Это отличный способ создания повторно используемых внутренних компонентов, которые соответствуют вашим рекомендациям по стилю, без перестройки целых HTML-элементов! Вы можете просто переопределить все реквизиты, такие как установка className
на основе статуса, или разрешить передачу дополнительных имен классов.
import React, { ButtonHTMLAttributes } from 'react' export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { status?: 'primary' | 'info' | 'danger' } export default function Button(props: ButtonProps) { const { status, children, className, ...rest } = props return ( <button className={`${status || ''} ${className || ''}`} {...rest} > {children} </button> ) }
Здесь мы берем className
переданное нашему элементу Button, и вставляем его обратно с проверкой безопасности в случае, если свойство не undefined
.
Заключение
React — чрезвычайно мощная библиотека, и есть веская причина, по которой она быстро завоевала популярность. Он дает вам отличный набор инструментов для создания производительных и простых в обслуживании веб-приложений. Он чрезвычайно гибкий и в то же время очень строгий, что может быть невероятно полезным, если вы знаете, как его использовать. Это всего лишь несколько API, которые заслуживают внимания и в значительной степени игнорируются. Попробуйте их в своем следующем проекте!
Для дальнейшего чтения о последних API-интерфейсах React и хуках я бы рекомендовал прочитать useHooks(). Шпаргалка Typescript также содержит полезную информацию о хуках React и Typescript.