Trabajando con React Hooks y TypeScript

Publicado: 2022-03-11

Los ganchos se introdujeron en React en febrero de 2019 para mejorar la legibilidad del código. Ya discutimos los ganchos de React en artículos anteriores, pero esta vez estamos examinando cómo funcionan los ganchos con TypeScript.

Antes de los ganchos, los componentes de React solían tener dos sabores:

  • Clases que manejan un estado
  • Funciones que están completamente definidas por sus props.

Un uso natural de estos era construir componentes de contenedor complejos con clases y componentes de presentación simples con funciones puras.

¿Qué son los ganchos de reacción?

Los componentes del contenedor manejan la administración de estado y las solicitudes al servidor, que luego se llamarán efectos secundarios en este artículo. El estado se propagará a los hijos del contenedor a través de los accesorios.

¿Qué son los ganchos de reacción?

Pero a medida que crece el código, los componentes funcionales tienden a transformarse en componentes contenedores.

Actualizar un componente funcional a uno más inteligente no es tan doloroso, pero es una tarea desagradable y que consume mucho tiempo. Además, ya no es bienvenido distinguir los presentadores y los contenedores de manera muy estricta .

Los ganchos pueden hacer ambas cosas, por lo que el código resultante es más uniforme y tiene casi todos los beneficios. Este es un ejemplo de cómo agregar un estado local a un componente pequeño que maneja una comilla.

 // 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 </> }

Hay una gran ventaja en esto: la codificación con TypeScript fue excelente con Angular pero inflada con React. Sin embargo, codificar ganchos de React con TypeScript es una experiencia agradable.

TypeScript con Old React

TypeScript fue diseñado por Microsoft y siguió el camino de Angular cuando React desarrolló Flow , que ahora está perdiendo fuerza. Escribir clases de React con TypeScript ingenuo fue bastante doloroso porque los desarrolladores de React tenían que escribir tanto props como state , aunque muchas teclas eran las mismas.

Aquí hay un objeto de dominio simple. Hacemos una aplicación de cotización , con un tipo de Quotation , administrada en algunos componentes crudos con estado y accesorios. Se puede crear la Quotation y su estado asociado puede cambiar a firmado o no.

 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> { // ... }

Pero imagine que QuotationPage ahora le pedirá al servidor una identificación : por ejemplo, la cotización 678 realizada por la empresa. Bueno, eso significa que QuotationProps no conoce ese número importante, no está envolviendo una cotización exactamente . Tenemos que declarar mucho más código en la interfaz de QuotationProps:

 interface QuotationProps{ // ... all the attributes of Quotation but id title:string; lines:QuotationLine[] price: number }

Copiamos todos los atributos excepto el id en un nuevo tipo. Hm. Eso me hace pensar en el antiguo Java, que implicaba escribir un montón de DTO. Para superar eso, aumentaremos nuestro conocimiento de TypeScript para evitar el dolor.

Beneficios de TypeScript con Hooks

Mediante el uso de ganchos, podremos deshacernos de la interfaz QuotationState anterior. Para hacer esto, dividiremos QuotationState en dos partes diferentes del estado.

 interface QuotationProps{ quotation:Quotation; } function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }

Al dividir el estado, no tenemos que crear nuevas interfaces. Los tipos de estado local a menudo se deducen de los valores de estado predeterminados.

Los componentes con ganchos son todas funciones. Entonces, podemos escribir el mismo componente que devuelve el tipo FC<P> definido en la biblioteca React. La función declara explícitamente su tipo de devolución, estableciendo junto con el tipo de props.

 const QuotationPage : FC<QuotationProps> = ({quotation}) => { const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }

Evidentemente, usar TypeScript con ganchos de React es más fácil que usarlo con clases de React. Y debido a que la escritura fuerte es una seguridad valiosa para la seguridad del código, debería considerar usar TypeScript si su nuevo proyecto usa ganchos. Definitivamente deberías usar ganchos si quieres algo de TypeScript.

Existen numerosas razones por las que podría evitar TypeScript, usando React o no. Pero si elige usarlo, definitivamente también debe usar ganchos.

Características específicas de TypeScript adecuado para ganchos

En el ejemplo anterior de TypeScript de ganchos de React, todavía tengo el atributo de número en QuotationProps, pero aún no hay idea de cuál es ese número en realidad.

TypeScript nos brinda una larga lista de tipos de utilidades, y tres de ellas nos ayudarán con React al reducir el ruido de muchas descripciones de interfaz.

  • Partial<T> : cualquier subclave de T
  • Omit<T, 'x'> : todas las claves de T excepto la clave x
  • Pick<T, 'x', 'y', 'z'> : Exactamente las teclas x, y, z de T

Características específicas de TypeScript adecuado para ganchos

En nuestro caso, nos gustaría que Omit<Quotation, 'id'> omitiera el id de la cita. Podemos crear un nuevo tipo sobre la marcha con la palabra clave type .

Partial<T> y Omit<T> no existen en la mayoría de los lenguajes escritos, como Java, pero son de gran ayuda para los ejemplos con Forms en el desarrollo front-end. Simplifica la carga de escribir.

 type QuotationProps= Omit<Quotation, id>; function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); // ... }

Ahora tenemos una cotización sin identificación. Entonces, tal vez podríamos diseñar una Quotation y PersistedQuotation extends Quotation . Además, resolveremos fácilmente algunos problemas recurrentes if o undefined . ¿Deberíamos seguir llamando a la cotización variable, aunque no es el objeto completo? Eso está más allá del alcance de este artículo, pero lo mencionaremos más adelante de todos modos.

Sin embargo, ahora estamos seguros de que no vamos a difundir un objeto que pensábamos que tenía un number . El uso de Partial<T> no ofrece todas estas garantías, así que utilícelo con discreción.

Pick<T, 'x'|'y'> es otra forma de declarar un tipo sobre la marcha sin la carga de tener que declarar una nueva interfaz. Si es un componente, simplemente edite el título de la cotización:

 type QuoteEditFormProps= Pick<Quotation, 'id'|'title'>

O solo:

 function QuotationNameEditor({id, title}:Pick<Quotation, 'id'|'title'>){ ...}

No me juzguen, soy un gran admirador del Diseño Impulsado por Dominios. No soy perezoso hasta el punto de no querer escribir dos líneas más para una nueva interfaz. Utilizo interfaces para describir con precisión los nombres de dominio y estas funciones de utilidad para la corrección del código local, evitando el ruido. El lector sabrá que Quotation es la interfaz canónica.

Otros beneficios de los ganchos de reacción

El equipo de React siempre vio y trató a React como un marco funcional. Usaron clases para que un componente pudiera manejar su propio estado, y ahora se enganchan como una técnica que permite que una función realice un seguimiento del estado del componente.

 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> ); }

Este es un caso en el que usar Partial es seguro y una buena opción.

Aunque una función se puede ejecutar varias veces, el useReducer asociado se creará solo una vez.

Al extraer naturalmente la función reductora del componente, el código se puede dividir en múltiples funciones independientes en lugar de múltiples funciones dentro de una clase, todas vinculadas al estado dentro de la clase.

Esto es claramente mejor para la capacidad de prueba: algunas funciones se ocupan de JSX, otras del comportamiento, otras de la lógica empresarial, etc.

(Casi) ya no necesita componentes de orden superior. El patrón Render props es más fácil de escribir con funciones.

Entonces, leer el código es más fácil. Tu código no es un flujo de clases/funciones/patrones sino un flujo de funciones. Sin embargo, debido a que sus funciones no están asociadas a un objeto, puede ser difícil nombrar todas estas funciones.

TypeScript sigue siendo JavaScript

JavaScript es divertido porque puedes romper tu código en cualquier dirección. Con TypeScript, aún puede usar keyof para jugar con las claves de los objetos. Puede usar uniones de tipos para crear algo ilegible e imposible de mantener; no, no me gustan. Puede usar el tipo de alias para fingir que una cadena es un UUID.

Pero puedes hacerlo con nula seguridad. Asegúrese de que su tsconfig.json tenga la opción "strict":true . Compruébelo antes del inicio del proyecto, ¡o tendrá que refactorizar casi todas las líneas!

Hay debates sobre el nivel de escritura que pones en tu código. Puede escribir todo o dejar que el compilador infiera los tipos. Depende de la configuración del linter y de las elecciones del equipo.

Además, ¡todavía puedes cometer errores de tiempo de ejecución! TypeScript es más simple que Java y evita problemas de covarianza/contravarianza con Generics.

En este ejemplo de Animal/Gato, tenemos una lista de animales que es idéntica a la lista de gatos. Desafortunadamente, es un contrato en la primera línea, no en la segunda. Luego, agregamos un pato a la lista de animales, por lo que la lista de gatos es falsa.

 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 solo tiene un enfoque bivariante para genéricos que es simple y ayuda a la adopción de desarrolladores de JavaScript. Si nombra sus variables correctamente, rara vez agregará un duck a una lista de listOfCats .

Además, hay una propuesta para agregar contratos de entrada y salida para covarianza y contravarianza.

Conclusión

Hace poco volví a Kotlin, que es un buen lenguaje escrito, y era complicado escribir genéricos complejos correctamente. No tiene esa complejidad genérica con TypeScript debido a la simplicidad de la escritura pato y el enfoque bivariante, pero tiene otros problemas. Quizás no encontró muchos problemas con Angular, diseñado con y para TypeScript, pero los tuvo con las clases de React.

TypeScript fue probablemente el gran ganador de 2019. Obtuvo React, pero también está conquistando el mundo del back-end con Node.js y su capacidad para escribir bibliotecas antiguas con archivos de declaración bastante simples. Está enterrando a Flow, aunque algunos llegan hasta el final con ReasonML.

Ahora, siento que hooks+TypeScript es más agradable y productivo que Angular. No hubiera pensado eso hace seis meses.