{G}eekrainian

Почему не стоит использовать React.FC

12 мин. чтения
Программирование

React.FC - это тип, который поставляется вместе с типами пакета @types/react для React. Он представляет собой тип функционального компонента, который является основным строительным блоком большинства современных приложений на React.

В то время как FC удобен в использовании и в некоторых случаях может быть полезным, есть несколько причин, почему его не следует использовать.

Определение типа React.FC

Давайте взглянем на определение типа FC на примере нескольких последних версий React.

// Тип FC - это сокращение от FunctionComponent
type FC<P = {}> = FunctionComponent<P>;
  1. React 16.x:
interface FunctionComponent<P = {}> {
  (props: P & { children?: ReactNode }, context?: any): ReactElement<any, any> | null;
  propTypes?: WeakValidationMap<P>;
  contextTypes?: ValidationMap<any>;
  defaultProps?: Partial<P>;
  displayName?: string;
}
  1. React 17.x:
type PropsWithChildren<P> = P & { children?: ReactNode };

interface FunctionComponent<P = {}> {
  (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
  propTypes?: WeakValidationMap<P>;
  contextTypes?: ValidationMap<any>;
  defaultProps?: Partial<P>;
  displayName?: string;
}
  1. React 18.x (github):
interface FunctionComponent<P = {}> {
  (props: P, context?: any): ReactNode;
  propTypes?: WeakValidationMap<P> | undefined;
  contextTypes?: ValidationMap<any> | undefined;
  defaultProps?: Partial<P> | undefined;
  displayName?: string | undefined;
}

Среди существенных отличий между версиями типа FC можно отметить:

  1. props теперь не имеет обертки PropsWithChildren (ранее наличие свойства children)

  2. Возвращаемое значение изменено с ReactElement<any, any> | null на ReactNode, что позволяет теперь возвращать простые типы без ошибки TypeScript (например, return 123)

Подробное описание свойств React.FC

  • propTypes и defaultProps определяют типы свойств на основе обобщённого типа P. Эти функции обеспечивают валидацию типов для свойств, которые вы передаете компоненту, и значения свойств по умолчанию, которые вы можете определить с помощью Component.defaultProps = {}.

  • contextTypes был частью старой системы контекста в React, которая использовалась для передачи данных глубоко вложенным компонентам без необходимости передавать промежуточные свойства через все промежуточные компоненты. Однако, начиная с React версии 16.3, рекомендуется использовать новый API контекста: React.createContext, contextType, и useContext.

  • displayName может пригодится при отладке кода.

Использование children

До выпуска React 18, тип FC имел неявное свойство children, что позволяло как передавать дочерние элементы так и оставлять компоненты без них. Это порой приводит к неочевидным ошибкам, так как однозначно непонятно должен ли компонент принимать children или нет. 🤔

Пример с FC (до React 18):

interface MyComponentProps {
  text: string;
}

const MyComponent: React.FC<MyComponentProps> = ({ text }) => <h1>Hello {text}</h1>;

// Применение компонента

<MyComponent text="World">
  {"Здесь не будет ошибки TypeScript т.к. children неявно определен в компоненте через FC! ✅"}
</MyComponent>

Пример без FC:

interface MyComponentProps {
  text: string;
}

const MyComponent = ({ text }: MyComponentProps): JSX.Element => <h1>Hello {text}</h1>;

// Применение компонента
// Ошибка: Property 'children' does not exist on type 'IntrinsicAttributes & MyComponentProps'.

<MyComponent text="World">
  {"Здесь будет ошибка TypeScript, т.к. children не определен в компоненте! ⚠️"}
</MyComponent>

Пример без FC но с добавлением children:

interface MyComponentProps {
  text: string;
  children?: React.ReactNode;
}

const MyComponent = ({ text }: MyComponentProps): JSX.Element => (
  <div>
    <h1>Hello {text}</h1>
    {children}
  </div>
);

// Применение компонента

<MyComponent text="World">
  {"Здесь не будет ошибки TypeScript т.к. children явно определен в компоненте! ✅"}
</MyComponent>

Использование defaultProps

Если вы используете FC, то свойство defaultProps должно быть определено внутри самой функции компонента, что может вызвать проблемы при типизации и автодополнении в TypeScript.

FC уже включает свойства children и props, а наружу ваши добавляемые свойства пытаются конфликтовать с этими внутренними свойствами. Также есть информация, что defaultProps будет помечен как "устаревший" тип в будущих версиях React.

В примере ниже, TypeScript не будет корректно распознавать defaultProps, и автодополнение не будет работать для text:

interface MyComponentProps {
  text: string;
}

const MyComponent: React.FC<MyComponentProps> = ({ text }) => <h1>Hello {text}</h1>;

MyComponent.defaultProps = {
  text: "World",
};

// Применение компонента
// Ошибка: Property 'text' is missing in type '{}' but required in type 'MyComponentProps'

{/* Здесь будет ошибка TypeScript ⚠️ */}
<MyComponent />

Пример без FC:

interface MyComponentProps {
  text: string;
}

const MyComponent = ({ text }: MyComponentProps): JSX.Element => <h1>Hello {text}</h1>;

MyComponent.defaultProps = {
  text: "World",
};

{/* Здесь не будет ошибки TypeScript ✅ */}
<MyComponent />

Вложенные компоненты

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

Пример компонента:

<Menu>
  <Menu.Item>Text</Menu.Item>
</Menu>

Пример реализации с FC:

const Select: React.FC<SelectProps> & { Item: React.FC<ItemProps> } = (props) => { /* ... */ }

Select.Item = (props) => { /* ... */ }

Пример реализации без FC:

const Select = (props: SelectProps) => { /* ... */ };

Select.Item = (props: ItemProps) => { /* ... */ };

Избыточные свойства

FC автоматически добавляет свойства, такие как propTypes, contextTypes и др., к вашему компоненту. Если вы не используете эти свойства, это может создать лишний шум в вашем коде.

Экспериментальный тип

Так как FC - это экспериментальный тип и не всегда документированный как часть официального API, его поведение или поддержка могут измениться в будущих версиях.

Миграция проектов с предыдущих версий React

Если вы задались вопросом переноса проекта на новую версию React 18 и выше, а в вашем проекте активно используется FC, вам понадобится добавить пару новых определений и сделать несколько замен чтобы добиться обратной совместимости.

  1. Добавьте определения типов в файл types.d.ts - это создаст отдельные версии FC/VFC с PropsWithChildren, тем самым, включая children в тип и делая его таким же каким он был в предыдущих версиях React.
// types.d.ts
import { FunctionComponent, PropsWithChildren } from 'react';

declare module 'react' {
  type FC17<P = {}> = FunctionComponent<PropsWithChildren<P>>;
  type VFC17<P = {}> = FunctionComponent<P>;
}
  1. Пройдитесь по всему коду и произведите замену FC на FC17 и VFC на VFC17. Пример компонента после замены:
import { FC17 } from 'react';

export const MyComponent: FC17 = ({ children }) => <div>{children}</div>;

Имейте ввиду, что данная манипуляция необходима только для переноса старых компонентов, не следует использовать это для нового кода. Используйте обновленную версию FC для новых компонентов.

Как убрать предупреждения TypeScript

В случае, если вам не нужно мигрировать компоненты и вы хотите просто убрать предупреждения TypeScript связанные с определением children, добавьте файл react.d.ts со следующим содержимым, которое вернет тип FC к версии React 17.

// react.d.ts
import { FunctionComponent, PropsWithChildren } from 'react';

declare module 'react' {
  interface FunctionComponent<P = {}> {
    (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
  }
}

Упрощенный переход на Preact

Preact - это легковесная альтернатива библиотеки React, которая имеет обратную совместимость с экосистемой React. Вы можете сэкономить до 100 КБ в размере продакшн-версии и использовать независимую библиотеку.

Preact написан на TypeScript (с аннотациями JSDoc), поэтому вы получаете всю информацию о типах как и в React. Поскольку все типы из пакета @types/react не совместимы с Preact, вам понадобится адаптировать ваш код таким образом, чтобы альтернативный тип принимал children, например:

// Альтернатива React.FC для Preact
type WithChildren<T = {}> = T & { children?: VNode };

Заключение

В некоторых проектах FC может быть полезным и не стоит его избегать при первой же возможности. Однако, если вы сталкиваетесь с проблемами в типизации, автодополнении или поддержке, рассмотрите возможность использования обычных функций или альтернативных типов или попробуйте перенести проект на React последней версии.

Дополнено: Facebook убрал тип React.FC из своего базового шаблона для проектов на TypeScript, так как это лишняя функциональность с минимальными преимуществами в сочетании с некоторыми недостатками.

Эта история оказалось полезной? 🤔

Поддержите меня чашечкой кофе и станьте спонсором нового контента!

BuyMeACoffee

Ko-Fi

Поделиться

Похожие публикации

Сравнение скорости установки пакетов Yarn и NPM

Сравниваем скорость установки пакетов Yarn и NPM на примере нескольких проектов разного размера... Читать далее

© geekrainian.com

  • Русский
  • English
RSSКарта сайта