Классовые компоненты¶
Помимо компонентов на основе функций, React позволяет определять компоненты на основе классов, которые, как в случае реализации ловушки для ошибок просто не заменимы. Кроме этого, с ними необходимо познакомиться, так как они являются частью React. Именно поэтому текущая глава полностью посвящена старым добрым классовым компонентам.
Производные от Component<P, S, SS>
¶
Пользовательские компоненты построенные на основе классов обязаны расширять базовый обобщенный класс Component<Props, State, Snapshot>
имеющего три необязательных параметра типа.
1 2 3 4 5 6 7 8 9 |
|
Первым делом стоит обратить внимание на первую строку, а именно импорт пространства имен React. Не зависимо используете вы его напрямую или нет, оно обязательно должно быть импортировано, в противном случаи компилятор напомнит об этом с помощью ошибки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Кроме того в нашем примере у метода render
отсутствует аннотация возвращаемого типа, что на практике даже приветствуется. Но с образовательной точки зрения её указание не принесет никакого вреда.
1 2 3 4 5 6 7 8 9 |
|
При переопределении производным классом метода render
в качестве типа возвращаемого значения необходимо указывать тип совместимый с указанным в базовом классе, то есть с типом ReactNode
поведение и нюансы которого были подробно рассмотрены в главе посвященной функциональным компонентам.
Как говорилось ранее, тип от которого должны наследоваться пользовательские классовые компоненты является обобщенным и имеет три необязательных параметра типа, что и иллюстрирует наш минималистический пример.
1 2 3 4 5 6 7 |
|
В реальных проектах подобное встречается редко, поэтому следующим шагом разберем логику определения типов описывающих пользовательский компонент.
Параметры Props
¶
Начнем по порядку, а именно с Props
. Несмотря на то что пропсы делятся на обязательные и необязательные, все они по мере необходимости передаются в качестве аргументов конструктора при создании его экземпляра и доступны по ссылке this.props
(обозначим их как общие пропсы). Тем не менее за инициализацию необязательных пропсов ответственен сам классовый компонент для чего и предусмотренно статическое поле defaultProps
.
1 2 3 4 5 6 7 8 9 10 11 |
|
Тот факт что аннотация defaultProps
предполагает тип представляющий лишь ассоциированное с этим полем значение вынуждает разделить декларацию общих пропсов на два типа DefaultProps
и Props
. Ввиду того что тип Props
представляет не только обязательные пропсы, но и необязательные, он должен расширять (extends
) тип DefaultProps
.
1 2 3 4 5 6 7 8 9 10 |
|
Не будет лишним упомянуть что в реальных проектах интерфейс Props
, помимо DefaultProps
, очень часто расширяет множество других интерфейсов. В их число входят типы, предоставляемые библиотеками ui, hoc обертками и обычными библиотеками, как например react-router и его тип RouteComponentProps<T>
.
Поскольку в описании базового класса поле (this.props
) принадлежит к типу определенного в качестве первого параметра типа, то есть Component<Props>
, то Props
необходимо указать в аннотации не только первого параметра конструктора, но и в качестве первого аргумента базового типа. Иначе this.props
так и останется принадлежать к простому объектному типу {}
заданному по умолчанию.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Как было сказано в теме посвященной функциональным компонентам, что если взять за правило именовать типы пропсов как DefaultProps
и Props
, то при необходимости в их импорте непременно возникнет коллизия из-за одинаковых имен. Поэтому принято добавлять к названиям названия самих компонентов *DefaultProps
и *Props
. Но поскольку эти типы повсеместно указываются в аннотациях расположенных в теле классового компонента, то подобные имена попросту усложняют понимание кода. Поэтому для исчерпывающих имен необходимо создавать более компактные псевдонимы типа type
.
Также стоит сразу сказать, что все три типа выступающих в качестве аргументов базового типа нуждаются в более компактных идентификаторах определяемых с помощью псевдонимов. Но кроме того, все они описывают объекты, мутация которых не предполагается. Простыми словами типы Props
, State
и Snapshot
используются исключительно в аннотациях readonly
полей класса, параметрах его методов и возвращаемых ими значениях. Поскольку секрет здорового приложения кроется в типобезопасности, всю упомянутую троицу необходимо сделать неизменяемой. Для этого существует специальный тип Readonly<T>
. Но так как преобразование типов в каждой отдельной аннотации приведет к чрезмерному увеличению кода, необходимо проделать это единожды в определении их псевдонимов.
Посмотрим как новая информация преобразит наш основной пример.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Параметр children
¶
Также стоит упомянуть что пропсы всех компонентов по умолчанию имеют определение необязательного (объявленного с модификатором ?:
) поля children
принадлежащего к оговоренному ранее типу ReactNode
. Простыми словами можно вообще не передавать аргументы базовому типу и компилятор не выдаст ошибку при обращении к полю this.props.children
;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
В остальном children
имеют то же поведение и недостатки подробно описанные в главе посвященной функциональным компонентам. Поэтому оставим их и приступим к рассмотрению второго параметра базового типа Component
, а именно к типу представляющего состояние компонента Component<Props, State>
.
Несмотря на то что состояние является закрытым от внешнего мира, тип представляющий его также принято называть с префиксом в роли которого выступает название самого компонента. Причина кроется не только в соблюдении общего стиля кода относительно именования типов пропсов. На практике могут возникнуть коллизии имен при создании вложенных классовых компонентов что является обычным делом при создании hoc. Поэтому для типа описывающего состояние компонента так же необходимо определить ещё и псевдоним и не забыть передать его в качестве второго аргумента базового типа и указать в аннотации поля state
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
Состояние State
¶
Пора обратить внимание на момент связанный с объявлением defaultProps
и state
, которым необходимо указывать (или не указывать вовсе) модификатор доступа public
, так как к ним должен быть доступ извне. Кроме того не будет лишним добавить этим полям модификатор readonly
, который поможет избежать случайных изменений.
Говоря о состоянии нельзя обойти стороной такой метод как setState
необходимый для его изменения, о котором известно что в качестве аргумента он может принимать как непосредственно объект представляющий новое состояние, так и функцию возвращающую его. Но поскольку первый случай ничего, что нас могло бы заинтересовать, из себя не представляет, рассмотрен будет лишь второй вариант с функцией. Поэтому продолжим наш основной пример и внесем в него изменения касающиеся изменения состояния. Создадим скрытый метод reset
который будет сбрасывать значение пройденного времени.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Из того кода что был добавлен в наш пример стоит обратить внимание на несколько моментов. Прежде всего это использование псевдонимов Props
и State
в аннотациях параметров функции переданной в метод setState
. Обозначим её как updater
. Как было сказано ранее, типы описывающие состояние и пропсы используются повсеместно в коде компонента. Кроме того стоит сказать что описание сигнатуры функции updater
подобным образом излишне и имеет место быт лишь в образовательных целях. Достаточно просто определить необходимые параметры и вывод типов самостоятельно определит их принадлежность.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
В добавок к этому стоит возложить определение возвращаемого значения из функции updater
на вывод типов, поскольку это не просто излишне, но и в большинстве случаев может являться причиной избыточного кода. Все дело в том что когда состояние содержит множество полей, обновление которых не производится одновременно, при указании возвращаемого типа как State
будет невозможно частичное обновление, поскольку лишь часть типа State
не совместимо с целым State
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
|
В случае когда функция updater
выполняет частичное обновление состояния и при этом тип возвращаемого значения указан явно, необходимо воспользоваться механизмом распространения (spread
) дополнив отсутствующую часть в новом состоянии старым.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
Несмотря на то что механизм распространения помогает обойти трудности связанные с совместимостью типов, лучшим вариантом будет вообще не указывать возвращаемый функцией updater
тип, а возложить эту обязанность на вывод типов.
И последнее о чем ещё не упомянули, что метод setState
, в качестве второго параметра принимает функцию обратного вызова, декларация которой очень проста и будет рассмотрена в самом конце данной главы, когда весь код будет собран в одном месте.
Snapshot¶
И на этом рассмотрение состояния завершено, поэтому можно приступить к рассмотрению третьего и последнего параметра базового типа Component<Props, State, Snapshot>
.
Принципы применяемые для описания типа представляющего Snapshot
ничем не отличаются от описания Props
и State
, поэтому пояснения будут опущены.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
Жизненный цикл¶
Ничего особенного на что стоило бы обратить внимание нет. Поэтому без лишних комментариев продолжим знакомство с внутренним устройством компонента - его жизненного цикла.
Погружение в типизированный жизненный цикл классовых компонентов необходимо начать с его разделения на две части - актуальный жизненный цикл и устаревший жизненный цикл, который будет исключён из рассмотрения. Поскольку в аннотации методов жизненного цикла не содержится ничего, что было бы не понятно к этому моменту, пояснение каждого отдельного случая будет опущено.
Обратить внимание стоит лишь на импорт впервые встречающегося типа ErrorInfo
необходимость в котором появляется при определении необязательно метода componentDidCatch
. Кроме того не будет лишнем напомнить, что в строгом, рекомендуемом режиме, при котором все элементы без аннотации неявно принадлежат к типу any
, аннотация сигнатур методов является обязательной. И по этому случаю ещё раз стоит упомянуть о пользе коротких псевдонимов заменяющих огромные идентификаторы типов *Props
, *State
и *Snapshot
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Вдобавок необходимо заметить, что код иллюстрирующий жизненный цикл компонента взять из декларации устанавливаемой из репозитория @types/react
и именно поэтому она изобилует излишними преобразованиями в Readonly<T>
тип. Но как было отмечено ранее, в этом нет нужны поскольку все типы составляющие троицу аргументов базового типа уже прошли преобразование при определении представляющих их псевдонимов. Учитывая этот факт предыдущий код будет выглядеть следующим образом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
Ссылки ref
¶
Следующий в очереди на рассмотрение механизм, получение ссылок на нативные dom элементы и React компоненты, обозначаемый как рефы (refs).
Предположим что существует форма которую по событию submit
необходимо очистить при помощи нативного метода reset
, доступного лишь через нативный dom элемент, ссылку на который можно получить с помощью механизма рефов, применение которого возможно осуществить двумя способами. Первый способ заключается в создании объекта реф с помощью статического метода React.createRef()
, а второй в самостоятельном сохранении ссылки на нативный dom элемент с помощью функции обратного вызова.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Начнем по порядку. Первым делом необходимо определить поле (в нашем случае это formRef
) необходимое для сохранения объекта реф и желательно чтобы оно было закрытое (private
) и только для чтения (readonly
). В примере поле formRef
определен вместе с аннотацией в которой указан импортированный тип RefObject<T>
, где параметр типа принимает тип нативного dom элемента, в нашем случае HTMLFormElement
. Но в конкретном примере аннотация излишня поскольку мы указали выводу типов принадлежность нативного dom элемента передав его в качестве аргумента типа функции React.createRef<T>()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
На следующим шаге устанавливаем объект реф react элементу <form>
и определяем закрытый метод reset
в котором происходит вызов метода reset
нативной формы. Не будет лишним обратить внимание, что вызов непосредственно метода reset
осуществляется при помощи оператора опциональной последовательности (?.
). Сделано это по причине возможного отсутствия ссылки на нативный элемент.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Второй способ получения ссылки на нативный элемент заключается в определении функции принимающей в качестве единственного параметра нативный dom элемент, сохранение ссылки на который перекладывается на разработчика.
Для иллюстрации сказанного повторим предыдущий пример. Первым делом импортируем обобщенный тип RefCallback<T>
описывающий функцию и принимающий в качестве аргумента типа тип нативного dom элемента который будет передан в функцию в качестве единственного аргумента. Затем определим поле formNativeElement
с типом union
, множество которого включат не только тип нативного элемента, но и null
. Это необходимо поскольку при инициализации требуется установить значение принадлежащие к типу null
. Это необходимо при активном флаге --strictPropertyInitialization
входящим в группировку определяющую рекомендуемый строгий режим компилятора.
Следующим шагом происходит определение закрытого только для чтения поля formRefCallback
которому в качестве значения присвоена стрелочная функция. Единственный параметр данной функции лишен аннотации тпа, поскольку вывод типов определит его как принадлежащего к переданному в качестве аргумента типа RefCallback<T>
. В теле данной функции происходит присваивание её параметра полю formNativeElement
определенному на предыдущем шаге.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Стоит заметить что то же самое можно реализовать и без помощи типа импортированного RefCallback<T>
. Для этого лишь потребуется самостоятельно добавить аннотацию типа для параметра функции обратного вызова.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Выбор того или иного способа зависит лишь от предпочтений самого разработчика.
Продолжим доведение примера до финального состояния и установим созданную в первом случае функцию обратного вызова react элементу <form>
в качестве реф. Также определим уже известный метод reset
в теле которого будет происходить вызов метода reset
у нативного dom элемента ссылка на который будет сохранена в поле класса formNativeElement
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
И раз уж тема дошла до рассмотрения рефов, то необходимо рассмотреть механизм получения с их помощью ссылки на классовый компонент.
Первым делом определим классовый компонент Slider
реализующий два открытых метода предназначенных для перелистывания контента prev
и next
. Далее определим компонент App
в теле которого определим рефу при помощи функции createRef
которой в качестве аргумента типа передадим тип классового компонента Slider
. Таким образом вывод типа определит рефу sliderRef
как принадлежащую к типу RefObject<Slider>
. После этого в методе рендер создадим экземпляр компонента Slider
и два react элемента <button>
, в обработчиках событий click
которых происходит взаимодействие с компонентом Slider
при помощи ссылки на него доступной через ассоциированную непосредственно с ним рефу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
Обработчики событий¶
На этом рассмотрение работы с механизмом рефов в типизированном стиле завершено. Но до завершения знакомства с работой классового компонента в основе которого лежит Component<Props, State, Snapshot>
осталась ещё одна тема, а именно работа с React событиями. Кроме того её освещение будет являться альтернативным решением задачи получения доступа к нативному элементу. Простыми словами реализуем вызов метода reset
у нативного dom элемента ссылку на который будет получена из объекта события submit
. Но поскольку данная тема была подробна рассмотрена в главе посвященной функциональным компонентам, здесь подробно будут освещены только моменты присущие исключительно классовым компонентам.
Первым делом возвратим предыдущий пример в первоначальное состояние и добавим кнопку для отправки формы.
1 2 3 4 5 6 7 8 9 10 11 |
|
Далее нам потребуется определить закрытое поле только для чтения в качестве значения которого будет присвоена стрелочная функция способная сохранить контекст текущего экземпляра. В качестве типа данного поля укажем импортированный из пространства имен React ранее рассмотренный обобщенный тип ReactEventHandler<T>
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
Для завершения примера осталось всего-навсего написать логику слушателя события submit
, которая также повторяет пример из главы посвященной функциональным компонентам и поэтому подробных комментариев не будет.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Данный способ типизирования слушателей событий является предпочтительным поскольку при таком подходе аннотация включает только два типа и кроме того, стрелочная функция уберегает от неминуемой потери контекста. Случаи требующие определения слушателя как метода класса требуют другого подхода. Отличие заключается в том что в аннотировании типа нуждается непосредственно параметр слушателя. Но поскольку React делегирует все нативные события, необходимо импортировать тип соответствующего события из его пространства имен. Для событий связанных с формами в React определен обобщенный тип FormEvent<T>
ожидающий в качестве аргумента типа тип нативного элемента. И поскольку слушатель ничего не возвращает, то тип возвращаемого значения, явное указание которого излишне, определяется как void
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Поскольку установка слушателя представляемого методом класса приведет к неминуемой потери контекста, прибегать к подобному объявлению стоит только при условии что их тело лишено логики предполагающей обращение к членам через ссылку экземпляра this
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Контекст можно было бы сохранить прибегнув к методу bind
или делегированию события непосредственно с помощью стрелочной функции определенной в месте установки слушателя, но зачем? Для bind
потребуется определения дополнительного поля.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Стрелочная функция будет пересоздаваться каждую отрисовку.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Кроме того оба случая затрудняют понимание кода. Поэтому необходимо повторить что использовать метод класса в качестве слушателя события стоит только при отсутствии необходимости в обращении через ссылку this
. При возникновении именно такого случая не будет лишним уточнения способа выбора типа события. В приведенном примере это был FormEvent<T>
, поскольку работа производилась с формой. Для других событий появится необходимость в других соответствующих типа, узнать которые можно с помощью подсказок вашей ide. Для чего всего-лишь необходимо навести курсор на определение слушателя события.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Также не забываем об упомянутом ранее базовом для всех событийных React типов обобщенном типе SyntheticEvent<T>
, который в качестве аргумента ожидает тип представляющий нативный элемент.
На этом тему посвященную созданию классового компонента расширяющего Component<Props, State, Snapshot>
можно заканчивать и переходить к следующей теме. Единственное что точно не будет лишним, так это собрать весь пройденный материал в одном месте.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
|
Производные от PureComponent<Props, State, Snapshot>
¶
Помимо того, что пользовательские компоненты могут быть производными от универсального класс Component<Props, State, Snapshot>
, они также могут использовать в качестве базового класса универсальный класс PureComponent<Props, State, Snapshot>
. Но поскольку все что было сказано относительно Component
в ста процентах случаев верно и для PureComponent
, который также ничего нового не привносит, то данная глава будет ограничена лишь кодом иллюстрирующим определение пользовательского компонента.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Тем кто только начал своё знакомство с классовыми компонентами с данной главы необходимо вернутся на шаг назад или даже более разумно в самое начало, поскольку именно там объясняется что для полного понимания необходимо ознакомиться со всем материалом относящимся к React.