Работа с хуками React и TypeScript
Опубликовано: 2022-03-11Хуки были введены в React в феврале 2019 года для улучшения читаемости кода. Мы уже обсуждали хуки React в предыдущих статьях, но на этот раз мы рассмотрим, как хуки работают с TypeScript.
До хуков компоненты React имели два варианта:
- Классы , которые обрабатывают состояние
- Функции , которые полностью определяются их реквизитами
Их естественным использованием было создание сложных компонентов-контейнеров с классами и простых презентационных компонентов с чистыми функциями.
Что такое React-хуки?
Компоненты-контейнеры обрабатывают управление состоянием и запросы к серверу, которые затем будут называться в этой статье побочными эффектами . Состояние будет передаваться дочерним элементам контейнера через реквизиты.
Но по мере роста кода функциональные компоненты имеют тенденцию трансформироваться в компоненты-контейнеры.
Модернизация функционального компонента на более умный не то чтобы болезненный, но трудоемкий и неприятный процесс. Более того, очень строгое разграничение презентеров и контейнеров больше не приветствуется.
Хуки могут делать и то, и другое, поэтому полученный код более однороден и обладает почти всеми преимуществами. Вот пример добавления локального состояния к небольшому компоненту, который обрабатывает подпись цитаты.
// put signature in local state and toggle signature when signed changes function QuotationSignature({quotation}) { const [signed, setSigned] = useState(quotation.signed); useEffect(() => { fetchPost(`quotation/${quotation.number}/sign`) }, [signed]); // effect will be fired when signed changes return <> <input type="checkbox" checked={signed} onChange={() => {setSigned(!signed)}}/> Signature </> }
В этом есть большой бонус — кодирование на TypeScript было отличным с Angular, но раздутым с React. Тем не менее, кодирование хуков React с помощью TypeScript — приятное занятие.
TypeScript со старым React
TypeScript был разработан Microsoft и пошел по пути Angular, когда React разработал Flow , который сейчас теряет популярность. Написание классов React с наивным TypeScript было довольно болезненным, потому что разработчикам React приходилось вводить как props
, так и state
, хотя многие ключи были одинаковыми.
Вот простой объект домена. Мы делаем приложение цитаты с типом Quotation
, управляемым некоторыми грубыми компонентами с состоянием и реквизитами. Quotation
может быть создано, и связанный с ним статус может измениться на подписанное или нет.
interface Quotation{ id: number title:string; lines:QuotationLine[] price: number } interface QuotationState{ readonly quotation:Quotation; signed: boolean } interface QuotationProps{ quotation:Quotation; } class QuotationPage extends Component<QuotationProps, QuotationState> { // ... }
Но представьте, что QuotationPage теперь будет запрашивать у сервера идентификатор : например, 678-е предложение, сделанное компанией. Что ж, это означает, что QuotationProps не знает этого важного числа — он не упаковывает цитату точно . Нам нужно объявить гораздо больше кода в интерфейсе QuotationProps:
interface QuotationProps{ // ... all the attributes of Quotation but id title:string; lines:QuotationLine[] price: number }
Мы копируем все атрибуты, кроме идентификатора, в новый тип. Хм. Это заставляет меня вспомнить старую Java, которая требовала написания множества DTO. Чтобы преодолеть это, мы расширим наши знания TypeScript, чтобы обойти эту боль.
Преимущества TypeScript с хуками
Используя хуки, мы сможем избавиться от прежнего интерфейса QuotationState. Для этого мы разделим QuotationState на две разные части состояния.
interface QuotationProps{ quotation:Quotation; } function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }
Разделив состояние, нам не нужно создавать новые интерфейсы. Типы локальных состояний часто определяются значениями состояния по умолчанию.
Компоненты с хуками — это все функции. Итак, мы можем написать тот же компонент, возвращающий тип FC<P>
, определенный в библиотеке React. Функция явно объявляет возвращаемый тип, устанавливая тип реквизита.
const QuotationPage : FC<QuotationProps> = ({quotation}) => { const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }
Очевидно, что использовать TypeScript с хуками React проще, чем использовать его с классами React. А поскольку строгая типизация является ценным средством обеспечения безопасности кода, вам следует рассмотреть возможность использования TypeScript, если в вашем новом проекте используются хуки. Вы обязательно должны использовать хуки, если хотите немного TypeScript.
Есть множество причин, по которым вы можете избегать TypeScript, используя React или нет. Но если вы решите использовать его, вам обязательно следует использовать и хуки.
Особенности TypeScript, подходящие для хуков
В предыдущем примере React перехватывает TypeScript, у меня все еще есть числовой атрибут в QuotationProps, но я еще не знаю, что это за число на самом деле.
TypeScript дает нам длинный список типов утилит, и три из них помогут нам с React, уменьшив шум многих описаний интерфейса.
-
Partial<T>
: любые вложенные ключи T -
Omit<T, 'x'>
: все ключи T, кроме ключаx
-
Pick<T, 'x', 'y', 'z'>
: Точноx, y, z
ключи отT
В нашем случае мы хотели бы, чтобы Omit<Quotation, 'id'>
опускал идентификатор цитаты. Мы можем создать новый тип «на лету» с помощью ключевого слова type
.
Partial<T>
и Omit<T>
не существуют в большинстве типизированных языков, таких как Java, но очень помогают в примерах с Forms во фронтенд-разработке. Это упрощает набор текста.
type QuotationProps= Omit<Quotation, id>; function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); // ... }
Теперь у нас есть цитата без идентификатора. Итак, возможно, мы могли бы разработать Quotation
, а PersistedQuotation
extends
Quotation
. Кроме того, мы легко решим некоторые повторяющиеся проблемы if
или undefined
. Должны ли мы по-прежнему вызывать переменную quotation, хотя это и не полный объект? Это выходит за рамки данной статьи, но мы все равно упомянем об этом позже.

Однако теперь мы уверены, что не будем распространять объект, который, как нам казалось, имел number
. Использование Partial<T>
не дает всех этих гарантий, поэтому используйте его с осторожностью.
Pick<T, 'x'|'y'>
— это еще один способ объявить тип на лету без необходимости объявлять новый интерфейс. Если это компонент, просто отредактируйте заголовок цитаты:
type QuoteEditFormProps= Pick<Quotation, 'id'|'title'>
Или просто:
function QuotationNameEditor({id, title}:Pick<Quotation, 'id'|'title'>){ ...}
Не судите меня, я большой поклонник Domain Driven Design. Я не ленив до того, что не хочу писать еще две строчки для нового интерфейса. Я использую интерфейсы для точного описания имен доменов, а эти служебные функции — для корректности локального кода, избегая шума. Читатель будет знать, что Quotation
— это канонический интерфейс.
Другие преимущества React Hooks
Команда React всегда рассматривала и рассматривала React как функциональную структуру. Они использовали классы, чтобы компонент мог обрабатывать свое собственное состояние, а теперь перехватчики как технику, позволяющую функции отслеживать состояние компонента.
interface Place{ city:string, country:string } const initialState:Place = { city: 'Rosebud', country: 'USA' }; function reducer(state:Place, action):Partial<Place> { switch (action.type) { case 'city': return { city: action.payload }; case 'country': return { country: action.payload }; } } function PlaceForm() { const [state, dispatch] = useReducer(reducer, initialState); return ( <form> <input type="text" name="city" onChange={(event) => { dispatch({ type: 'city',payload: event.target.value}) }} value={state.city} /> <input type="text" name="country" onChange={(event) => { dispatch({type: 'country', payload: event.target.value }) }} value={state.country} /> </form> ); }
Вот случай, когда использование Partial
безопасно и является хорошим выбором.
Хотя функция может выполняться множество раз, связанный с ней хук useReducer
будет создан только один раз.
Путем естественного извлечения функции редуктора из компонента код может быть разделен на несколько независимых функций вместо нескольких функций внутри класса, и все они связаны с состоянием внутри класса.
Это явно лучше для тестируемости — одни функции имеют дело с JSX, другие — с поведением, третьи — с бизнес-логикой и так далее.
Вам (почти) больше не нужны компоненты высшего порядка. Шаблон Render props проще написать с помощью функций.
Таким образом, чтение кода становится проще. Ваш код — это не поток классов/функций/шаблонов, а поток функций. Однако, поскольку ваши функции не привязаны к объекту, может быть сложно назвать все эти функции.
TypeScript — это все еще JavaScript
JavaScript — это весело, потому что вы можете развернуть свой код в любом направлении. С TypeScript вы по-прежнему можете использовать keyof
для игры с ключами объектов. Вы можете использовать объединения типов, чтобы создать что-то нечитаемое и неподдерживаемое — нет, мне это не нравится. Вы можете использовать псевдоним типа, чтобы представить строку как UUID.
Но вы можете сделать это с нулевой безопасностью. Убедитесь, что ваш tsconfig.json
имеет параметр "strict":true
. Проверьте это перед стартом проекта, иначе вам придется рефакторить почти каждую строчку!
Ведутся споры об уровне набора текста, который вы вкладываете в свой код. Вы можете ввести все или позволить компилятору вывести типы. Это зависит от конфигурации линтера и выбора команды.
Кроме того, вы все еще можете делать ошибки во время выполнения! TypeScript проще, чем Java, и позволяет избежать проблем ковариантности/контравариантности с дженериками.
В этом примере Animal/Cat у нас есть список Animal, который идентичен списку Cat. К сожалению, это контракт в первой линии, а не во второй. Затем мы добавляем утку в список животных, чтобы список кошек был ложным.
interface Animal {} interface Cat extends Animal { meow: () => string; } const duck = {age: 7}; const felix = { age: 12, meow: () => "Meow" }; const listOfAnimals: Animal[] = [duck]; const listOfCats: Cat[] = [felix]; function MyApp() { const [cats , setCats] = useState<Cat[]>(listOfCats); // Here the thing: listOfCats is declared as a Animal[] const [animals , setAnimals] = useState<Animal[]>(listOfCats) const [animal , setAnimal] = useState(duck) return <div onClick={()=>{ animals.unshift(animal) // we set as first cat a duck ! setAnimals([...animals]) // dirty forceUpdate } }> The first cat says {cats[0].meow()}</div>; }
TypeScript имеет только двухвариантный подход к дженерикам, который прост и помогает разработчикам JavaScript. Если вы правильно назовете свои переменные, вы редко добавите duck
в listOfCats
.
Кроме того, есть предложение по добавлению и исключению контрактов на ковариантность и контравариантность.
Заключение
Недавно я вернулся к Kotlin, который является хорошим типизированным языком, и было сложно правильно набирать сложные дженерики. У вас нет этой общей сложности с TypeScript из-за простоты утиного набора текста и бивариантного подхода, но у вас есть другие проблемы. Возможно, вы не сталкивались с большим количеством проблем с Angular, разработанным с помощью TypeScript и для него, но они были с классами React.
TypeScript, вероятно, стал большим победителем 2019 года. Он выиграл у React, но также завоевывает мир серверной части с Node.js и его способностью печатать старые библиотеки с довольно простыми файлами объявлений. Он хоронит Flow, хотя некоторые доходят до ReasonML.
Теперь я чувствую, что хуки + TypeScript приятнее и продуктивнее, чем Angular. Полгода назад я бы и не подумал.