Поддержка React.forwardRef и не только
В Flow v0.89.0 мы выпускаем React.AbstractComponent
, новый тип, который мы используем для моделирования forwardRef
и других компонентов React. Это новое представление совместимо с нашими текущими типами React, а также исправляет несколько ошибок в нашем предыдущем представлении. Вы можете найти документацию для React.AbstractComponent
здесь, а инструкции по ее использованию в HOC здесь. В этом сообщении блога будет кратко рассказано о React.AbstractComponent
, о том, как использовать React.forwardRef
в Flow, и о том, как сделать существующие HOC более безопасными с помощью React.AbstractComponent.
Фон
В оставшейся части этого поста я буду ссылаться на «тип экземпляра» компонента. Для функционального компонента тип экземпляра - void
, а для компонента класса тип экземпляра - это экземпляр класса. Когда вы передаете ссылку компоненту, поле current
имеет тип экземпляра компонента.
const ref: {current: null | HTMLButtonElement} = // ^^^ ref ^^^^^^^^^^Instance type React.createRef<HTMLButtonElement>(); <button ref={ref} />; // ^^Object with a current field
Для получения дополнительной информации о ссылках и экземплярах я рекомендую прочитать React docs.
Введение некоторых новых API React потребовало, чтобы мы переосмыслили то, как мы моделируем компоненты React в Flow. В нашей новой модели мы делаем упор на конфигурацию компонента, а не на свойства. Тип конфигурации компонента - это тип свойств со всеми свойствами, указанными в defaultProps, отмеченными как необязательные. Возьмем этот пример:
type Props = {foo: number, bar: number}; type DefaultProps = {foo: number}; class Component extends React.Component<Props> { static defaultProps: DefaultProps = {foo: 3}; } // Since `foo` is specified in the defaultProps, `foo` is // optional in the config of the component. // Therefore, the type of the config of `Component` is: type Config = {foo?: number, bar: number};
Это новое представление также обнаруживает многие ошибки, которые мы упускали раньше.
Другой целью нашего дизайна было предоставить абстракцию, которая поможет Flow лучше синхронизироваться с изменениями React. Хотя React.AbstractComponent
был в первую очередь предназначен для адреса forwardRef
, мы обнаружили, что его также можно использовать для ввода Fragment
, Suspense
, StrictMode
, ConcurrentMode
, memo
и lazy
. Мы надеемся, что в будущем он также сможет моделировать новые компоненты React.
Чтобы начать пользоваться некоторыми преимуществами нового представления, вам не нужно вносить какие-либо изменения в кодовую базу (кроме обновления версии Flow). В 0.89.0 мы заменяем определение React.ComponentType
на псевдоним для React.AbstractComponent
вместо объединения компонентов функции и класса. Это изменение может найти ошибки в вашем коде. В большинстве этих ошибок будут отсутствовать необходимые реквизиты и defaultProps
не соответствовать типам, указанным в Props
. Обязательно используйте флаг --show-all-branches
, если какие-либо ошибки кажутся особенно странными. --show-all-branches
дает больше информации об ошибках с типами объединения, которые мы используем для моделирования некоторых типов React. У нас также есть работа над улучшением некоторых сообщений об ошибках, связанных с реагированием, так что следите за обновлениями!
Понимание React.AbstractComponent
Кратко остановимся на том, как работает React.AbstractComponent
. Возьмем следующий пример:
//@flow type Props = {foo: number, bar: number}; type DefaultProps = {foo: number}; class ClassComponent extends React.Component<Props> { static defaultProps: DefaultProps = {foo: 3} }
ClassComponent
имеет свойства типа Props
и defaultProps
типа DefaultProps
, поэтому его тип конфигурации {foo?: number, bar: number}
. Элемент ClassComponent
имеет тип экземпляра ClassComponent
. Вот как это взаимодействует с React.AbstractComponent
:
(ClassComponent: React.AbstractComponent< {foo?: number, bar: number}, ClassComponent, >); // This is safe!
Обратите внимание, что foo
является необязательным в конфигурации, поскольку он указан в defaultProps
.
Поскольку вам может потребоваться аннотировать Config
в коде, но, вероятно, записан только тип Props
, мы предоставляем утилиту для вычисления типа Config
из Props
и DefaultProps
.
(ClassComponent: React.AbstractComponent< React.Config<Props, DefaultProps>, // Calculate the config ClassComponent, >); // This is safe!
React.forwardRef Поддержка
Благодаря нашей недавно появившейся поддержке forwardRef
вы можете безопасно пересылать ссылки в компонентах React и всегда быть уверены, что тип экземпляра компонента соответствует вашим ожиданиям.
Вы заметите, что если вы попытаетесь экспортировать forwardRef
компонент без каких-либо аннотаций, Flow запросит у вас одну:
//@flow const React = require('react'); type Props = { foo: number }; class Button extends React.Component<Props> {} // Error, missing annotation for Config and Instance in forwardRef module.exports = React.forwardRef( (props, ref) => <Button ref={ref} {...props} />, );
[Try-Flow]
Как и в случае с классом или функциональным компонентом, мы рекомендуем вам использовать псевдоним типа, чтобы указать свойства для вашего компонента и использовать этот псевдоним в качестве аргумента типа для forwardRef
.
//@flow const React = require('react'); type Props = { foo: number }; class Button extends React.Component<Props> {} module.exports = React.forwardRef< Props, Button, >((props, ref) => <Button ref={ref} {...props} />, );
[Try-Flow]
Если ваш компонент, который принимает ссылку, является классом, вы можете использовать экземпляр класса для аннотации экземпляра (как в примере выше).
Если ваш компонент сам по себе React.AbstractComponent<Config, Instance>
, используйте Instance
.
//@flow const React = require('react'); function wrapInDivPreserveInstance<Config: {}, Instance>( Component: React.AbstractComponent<Config, Instance> ): React.AbstractComponent<Config, Instance> { return React.forwardRef<Config, Instance>( (props, ref) => <div><Component ref={ref} {...props} /></div>, ); }
Если ваш компонент является встроенным, например div
или button
, используйте тип, определенный в dom.js
libdef, например HTMLDivElement
или HTMLButtonElement
.
//@flow const React = require('react'); type Props = { foo: number }; module.exports = React.forwardRef<Props, HTMLDivElement>( (props, ref) => <span><div ref={ref} /></span>, );
Если ваш компонент является функцией, вы, вероятно, захотите переосмыслить использование forwardRef
, поскольку тип экземпляра функционального компонента - void
.
Использование React.AbstractComponent для компонентов более высокого порядка
Используя React.AbstractComponent
, мы можем добиться большей точности при проверке компонентов React и HOC.
React.AbstractComponent
устраняет необходимость использовать React.ElementConfig
в HOC. Давайте посмотрим на обычную сигнатуру для HOC и посмотрим, как мы можем изменить ее, чтобы использовать React.AbstractComponent
для большей безопасности.
function HOC<TProps: {}, TComponent: React.ComponentType<TProps>>( Component: TComponent ): React.ComponentType<React.ElementConfig<TComponent>> { // ... }
Этот HOC принимает TComponent
и возвращает компонент с той же конфигурацией. Что ж, поскольку React.ElementConfig
больше не нужен, мы можем от него избавиться:
// We can completely get rid of React.ElementConfig in this type! function HOC<Config: {}>( Component: React.ComponentType<Config>, ): React.ComponentType<Config> { // ... }
Но React.ComponentType<Config>
- это псевдоним типа для React.AbstractComponent<Config, any>
. Мы можем получить еще более точные типы, если будем использовать React.AbstractComponent
напрямую.
Многие HOC заключают компонент в функцию, поэтому давайте рассмотрим этот случай более подробно:
function HOC<Config: {}>( Component: React.ComponentType<Config>, ): React.ComponentType<Config> { return props => <Component {...props} />; }
В качестве первого прохода мы можем просто заменить React.ComponentType
на React.AbstractComponent
, чтобы получить mixed
в качестве типа экземпляра вместо any
:
function HOC<Config: {}>( Component: React.AbstractComponent<Config>, ): React.AbstractComponent<Config> { return props => <Component {...props} />; }
Но мы знаем, что тип экземпляра функции всегда void
, поэтому мы можем получить еще более точную информацию, явно используя это в возвращаемом типе:
function HOC<Config: {}>( Component: React.AbstractComponent<Config>, ): React.AbstractComponent<Config, void> { return props => <Component {...props} />; }
Выполнение этого упражнения на собственном HOC может выявить ошибки недостающих свойств и места, где вы используете ref
неожиданным образом. Попробуйте и посмотрите, что вы откроете!
Заключение
Этот новый тип имеет множество преимуществ, в том числе:
- Больше поддерживаемых функций реакции
- Более точная проверка типов
- Более выразительные HOC
… И при этом не требует никаких кодмодов, чтобы начать его использовать. Мы рады видеть, что вы с его помощью построите!