25 апр. 2021 г., 15:22
Зачем для функциональных реакт-компонентов прописывать тип React.FC
Всем привет!
Данный материал довольно сложный, если вы еще не подружились с TypeScript и React, но он очень полезный.
Итак, прозвучал вопрос про React.FC: "Как бы ещё понять, что это за тит такой и когда его использовать?". Отвечаю подробно в топике, потому что касается многих. Рассмотрим вот такой пример (поиграть с ним можно здесь):
Первый вариант (то есть 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 - это динамический тип, который может быть переопределен извне, на уровне конечного вызова. А вот дальше обратите внимание на эту строку:
То есть далее тип P передается в свойства props. А если точнее разобрать эту строку, то это суть определение типа нашей конечной функции реакт-компонента. То есть здесь буквально говорится, что функция с параметрами (props: PropsWithChildren<P> (пропс - обязательный), context?: any (контекст - не обязательный)) при вызове вернет ReactElement<any, any> | null (то есть рекат-элемент или null). Собственно, PropsWithChildren<P> и добавляет нам по умолчанию в свойства children (что мы видели выше на скринах), хотя мы ничего изначально не передавали. Вот код:
Как видите, это тоже дженерик, который на выходе возвращает объединенный P и children.
То есть если мы пишем
то это буквально
ведь у нас P по умолчанию - пустой объект.
Но мы можем написать так:
В данном случае у нас P станет type {content: string}, а значит в свойствах помимо children мы можем ожидать еще и content с типом string (строка). При этом мы указали обязательный параметр, а значит если кто попытается вызывать наш компонент, но не передаст этот параметр (или передаст другой не совместимый по типу параметр), он получит ошибку, что мы и видели в примере с Component4.
Property 'content' is missing in type '{}' but required in type '{ content: string; }'
Но здесь нам стоит обратить внимание еще на одну строку:
defaultProps?: Partial<P>
Она наделяет наш реакт-компонент возможностью через статическое свойство задать пропсы по умолчанию. То есть это такие свойства, которые если не были переданы при вызове компоненна, используются по умолчанию. Пример:
В данном случае мы можем передавать свойство content в вызов <Component5 />, а можем не передавать. Если передаем, то используется наш вариант, если нет - то тот, что прописан по умолчанию. Только следует отметить, что указание значений по умолчанию не снимает обязательности с них, если они прописаны как обязательные, то есть тогда TS все равно будет требовать их передачу, поэтому здесь я указал content? (со знаком вопроса), то есть пометил этот параметр как не обязательный.
Если бы я не задал компоненту MainPage тип Page, то я не мог бы прописать функцию MainPage.getInitialProps.
Все это может показаться слишком запутанным и ненужным, но поверьте, это как минимум не ненужно. А запутанность с практикой исчезает. Зато потом огромный выхлоп в том, что вы легко видите какие параметры и каких типов можно передавать и что следует ожидать при вызове сторонних компонентов и функций, и не надо для этого лезть в чужой код и изучать весь его (что бывает очень сложно из-за множественных вложенностей).
UPD: Еще вот для затравки, зачем стоит осваивать TypeScript. Вот пример типозащищенной формы:
То есть здесь я могу указать только те поля, что зарегистированы в заданном типе. И если вдруг потом что-то изменится и зацепит эту форму, TS сообщит об этом.
Это функционал обновленной сборки, которую выложу вот буквально уже.