Николай
25 апр. 2021 г., 15:22

Зачем для функциональных реакт-компонентов прописывать тип React.FC

Всем привет!

Данный материал довольно сложный, если вы еще не подружились с TypeScript и React, но он очень полезный.

Итак, прозвучал вопрос про React.FC: "Как бы ещё понять, что это за тит такой и когда его использовать?". Отвечаю подробно в топике, потому что касается многих. Рассмотрим вот такой пример (поиграть с ним можно здесь):
import React from 'react'; const Component = () => { return ["some string", "some string 2"]; } /* Error Type '() => string[]' is not assignable to type 'FC<{}>'. Type 'string[]' is missing the following properties from type 'ReactElement<any, any>': type, props, key(2322) */ const Component2: React.FC = () => { return ["some string", "some string 2"]; } const Component3: React.FC = () => { return <p>some string</p>; } const Component4: React.FC<{content: string}> = ({content}) => { return <p> {content} </p>; } const Component5: React.FC<{content?: string}> = ({content}) => { return <p> {content} </p>; } Component5.defaultProps={ content: "Default content" } /* Error 'Component' cannot be used as a JSX component. Its return type 'string[]' is not a valid JSX element. Type 'string[]' is missing the following properties from type 'Element': type, props, key */ const el = <Component /> const el_OK = Component(); const el2 = <Component2 /> const el3 = <Component3 /> /* Error Property 'content' is missing in type '{}' but required in type '{ content: string; }' */ const el4 = <Component4 /> const el4_OK = <Component4 content="some string"/> const el5 = <Component5 />

Первый вариант (то есть const Component = ...) у нас объявлен без ошибок. Но вот когда мы его пытаемся выполнить как реакт-компонент (const el = <Component />), мы получаем тайпскриптовую ошибку:
'Component' cannot be used as a JSX component. Its return type 'string[]' is not a valid JSX element. Type 'string[]' is missing the following properties from type 'Element': type, props, key

Смысл в том, что мы в своей функции прописали в качестве результата массив строк. Тайпскрипт заранее не знает где и как мы собираемся использовать нашу функцию, он только видит, что у нас функция прописана корректно. Но когда мы ее пытаемся использовать именно как рендеринг реакт-компонента (то есть выполняем вызов), вот тут тайпскрипт нам говорит "В данной системе ожидается, что рендер должен возвращать один JSX.Element, а у вас функция возвращает массив строк". Вот это и есть ошибка типизации. То есть технически именно в таком виде это сработает. То есть если мы в рендер реакта пропишем <Component />, он выведет нам эти строки из массива. Но если мы заходим присвоить результат переменной и поработать с этим результатом, то тут у нас возникнут проблемы. Вот смотрите какие свойства есть у результата выполнения реакт-компонента, возвращающего массив строк:

То есть это нативные средства массива Array.

А вот какие свойства у правильного JSX.Element


Вот тут мы уже можем узнать его тип и получить его входящие свойства.


Второй момент - это передаваемые в компонент свойства. По-умолчанию в FC можно передать только key и children.


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


То есть в данном случае мы можем рассчитывать только на children и ни на что другое. То есть мы здесь четко знаем, что нам ничего другого не передадут сюда (ну, на самом деле могут, игнорируя ошибки и передать, но мы же не будем создавать у себя ошибки, а просто проигнорим такие лишние переданные параметры, верно?).

Собственно, для этого типизацию и вводят, чтобы было понятно где что мы ожидаем и где что можно передать.

Но как нам передать в компонент какие-то нужные нам параметры? Вот здесь мы откроем для себя тайпскриптовые дженерики. Вот про них отличная статья на хабре со всякими разъясняющими анимациями: https://habr.com/ru/post/455473/

Если мы поставим курсор в React.FC и нажмем F12, то перейдем вот сюда:


Как мы видим, FC - это по сути просто сокращенный алиас интерфейса FunctionComponent (здесь отмечу, что как я не искал, не нашел разницы между интерфейсами и типами, кроме как синтаксис, так что пока я не найду реально разницы, будем считать, что типы и интерфейсы в TS - суть одно и то же).

Так вот, конструкция FC<P = {}> - это и есть дженерик. Здесь P - это динамический тип, который может быть переопределен извне, на уровне конечного вызова. А вот дальше обратите внимание на эту строку:
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
То есть далее тип P передается в свойства props. А если точнее разобрать эту строку, то это суть определение типа нашей конечной функции реакт-компонента. То есть здесь буквально говорится, что функция с параметрами (props: PropsWithChildren<P> (пропс - обязательный), context?: any (контекст - не обязательный)) при вызове вернет ReactElement<any, any> | null (то есть рекат-элемент или null). Собственно, PropsWithChildren<P> и добавляет нам по умолчанию в свойства children (что мы видели выше на скринах), хотя мы ничего изначально не передавали. Вот код:
type PropsWithChildren<P> = P & { children?: ReactNode };
Как видите, это тоже дженерик, который на выходе возвращает объединенный P и children.

То есть если мы пишем
const Component3: React.FC = () => { return <p>some string</p>; }
то это буквально
const Component3: React.FC<{}> = () => { return <p>some string</p>; }
ведь у нас P по умолчанию - пустой объект.

Но мы можем написать так:
const Component4: React.FC<{content: string}> = ({content}) => { return <p> {content} </p>; }
В данном случае у нас P станет type {content: string}, а значит в свойствах помимо children мы можем ожидать еще и content с типом string (строка). При этом мы указали обязательный параметр, а значит если кто попытается вызывать наш компонент, но не передаст этот параметр (или передаст другой не совместимый по типу параметр), он получит ошибку, что мы и видели в примере с Component4.

Property 'content' is missing in type '{}' but required in type '{ content: string; }'


Но здесь нам стоит обратить внимание еще на одну строку:
defaultProps?: Partial<P>

Она наделяет наш реакт-компонент возможностью через статическое свойство задать пропсы по умолчанию. То есть это такие свойства, которые если не были переданы при вызове компоненна, используются по умолчанию. Пример:

const Component5: React.FC<{content?: string}> = ({content}) => { return <p> {content} </p>; } Component5.defaultProps={ content: "Default content" }
В данном случае мы можем передавать свойство content в вызов <Component5 />, а можем не передавать. Если передаем, то используется наш вариант, если нет - то тот, что прописан по умолчанию. Только следует отметить, что указание значений по умолчанию не снимает обязательности с них, если они прописаны как обязательные, то есть тогда TS все равно будет требовать их передачу, поэтому здесь я указал content? (со знаком вопроса), то есть пометил этот параметр как не обязательный.

Собственно, такой же принцип используется у нас в сборке nextjs в типе Page, чтобы в компонентах можно было задавать статическую функцию getInitialProps, в которой можно прописать дополнительно асинхронный код для получения нужных для страницы данных. Пример:
import { NextSeo } from 'next-seo' import { Page } from '../_App/interfaces' export const MainPage: Page = () => { return ( <> <NextSeo title="Main page" description="Main page description" /> <div>Main Page</div> </> ) } /** * Get data before render page */ MainPage.getInitialProps = () => { return { statusCode: 200, } } export default MainPage

Если бы я не задал компоненту MainPage тип Page, то я не мог бы прописать функцию MainPage.getInitialProps.


Все это может показаться слишком запутанным и ненужным, но поверьте, это как минимум не ненужно. А запутанность с практикой исчезает. Зато потом огромный выхлоп в том, что вы легко видите какие параметры и каких типов можно передавать и что следует ожидать при вызове сторонних компонентов и функций, и не надо для этого лезть в чужой код и изучать весь его (что бывает очень сложно из-за множественных вложенностей).


UPD: Еще вот для затравки, зачем стоит осваивать TypeScript. Вот пример типозащищенной формы:


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

Это функционал обновленной сборки, которую выложу вот буквально уже.

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