Поддержка 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

… И при этом не требует никаких кодмодов, чтобы начать его использовать. Мы рады видеть, что вы с его помощью построите!