Хуки были введены в React в феврале 2019 года для улучшения читаемости кода. На этот раз мы исследуем, как хуки работают с 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 теперь запросит у сервера id: например, 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, и позволяет избежать проблем ковариантности/контравариантности с дженериками.

В этом примере с животными/кошками у нас есть список животных, который идентичен списку кошек. К сожалению, это контракт в первой линии, а не во второй. Затем мы добавляем утку в список животных, чтобы список кошек был ложным.

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. Я бы не подумал об этом еще полгода назад.