Configuración de TypeScript para proyectos modernos de React usando Webpack
Publicado: 2022-03-10En esta era de desarrollo de software, JavaScript se puede usar para desarrollar casi cualquier tipo de aplicación. Sin embargo, el hecho de que JavaScript se escriba de forma dinámica podría ser una preocupación para la mayoría de las grandes empresas, debido a su característica de verificación de tipo flexible.
Afortunadamente, no tenemos que esperar hasta que el Comité Técnico 39 de Ecma introduzca un sistema de tipos estáticos en JavaScript. Podemos usar TypeScript en su lugar.
JavaScript, al estar tipado dinámicamente, no es consciente del tipo de datos de una variable hasta que se crea una instancia de esa variable en tiempo de ejecución. Los desarrolladores que escriben grandes programas de software pueden tener una tendencia a reasignar una variable, declarada anteriormente, a un valor de un tipo diferente, sin advertencia ni problema alguno, lo que genera errores que a menudo se pasan por alto.
En este tutorial, aprenderemos qué es TypeScript y cómo trabajar con él en un proyecto de React. Al final, habremos creado un proyecto que consiste en una aplicación de selección de episodios para el programa de televisión Money Heist , utilizando TypeScript y ganchos similares a React actuales ( useState
, useEffect
, useReducer
, useContext
). Con este conocimiento, puede continuar experimentando con TypeScript en sus propios proyectos.
Este artículo no es una introducción a TypeScript. Por lo tanto, no revisaremos la sintaxis básica de TypeScript y JavaScript. Sin embargo, no tienes que ser un experto en ninguno de estos idiomas para seguir, porque intentaremos seguir el principio KISS (mantenlo simple, estúpido).
¿Qué es TypeScript?
En 2019, TypeScript se clasificó como el séptimo lenguaje más utilizado y el quinto lenguaje de más rápido crecimiento en GitHub. Pero, ¿qué es exactamente TypeScript?
De acuerdo con la documentación oficial, TypeScript es un superconjunto escrito de JavaScript que se compila en JavaScript simple. Es desarrollado y mantenido por Microsoft y la comunidad de código abierto.
"Superconjunto" en este contexto significa que el lenguaje contiene todas las características y funciones de JavaScript y algo más. TypeScript es un lenguaje de secuencias de comandos escrito.
Ofrece a los desarrolladores más control sobre su base de código a través de su anotación de tipo, clases e interfaz, lo que evita que los desarrolladores tengan que corregir manualmente los molestos errores en la consola.
TypeScript no se creó para alterar JavaScript. En su lugar, se expande en JavaScript con valiosas funciones nuevas. Cualquier programa escrito en JavaScript simple también se ejecutará como se esperaba en TypeScript, incluidas las aplicaciones móviles multiplataforma y los back-ends en Node.js.
Esto significa que también puede escribir aplicaciones React en TypeScript, como haremos en este tutorial.
¿Por qué mecanografiar?
Quizás no esté convencido de aceptar las bondades de TypeScript. Consideremos algunas de sus ventajas.
Menos errores
No podemos eliminar todos los errores en nuestro código, pero podemos reducirlos. TypeScript comprueba los tipos en tiempo de compilación y arroja errores si cambia el tipo de variable.
Ser capaz de encontrar estos errores obvios pero frecuentes tan pronto hace que sea mucho más fácil administrar su código con tipos.
La refactorización es más fácil
Probablemente a menudo desee refactorizar muchas cosas, pero debido a que tocan muchos otros códigos y muchos otros archivos, desconfía de modificarlos.
En TypeScript, estas cosas a menudo se pueden refactorizar con solo un clic en el comando "Renombrar símbolo" en su entorno de desarrollo integrado (IDE).
En un lenguaje de escritura dinámica como JavaScript, la única forma de refactorizar varios archivos al mismo tiempo es con la función tradicional de "buscar y reemplazar" usando expresiones regulares (RegExp).
En un lenguaje de escritura estática como TypeScript, ya no se necesita "buscar y reemplazar". Con comandos IDE como "Buscar todas las apariciones" y "Renombrar símbolo", puede ver todas las apariciones en la aplicación de la función, clase o propiedad dada de una interfaz de objeto.
TypeScript lo ayudará a encontrar todas las instancias del bit refactorizado, cambiarle el nombre y alertarlo con un error de compilación en caso de que su código tenga algún tipo de discrepancia después de la refactorización.
TypeScript tiene incluso más ventajas que las que hemos cubierto aquí.
Desventajas de TypeScript
Seguramente TypeScript no está exento de desventajas, incluso dadas las características prometedoras destacadas anteriormente.
Una falsa sensación de seguridad
La función de verificación de tipos de TypeScript a menudo crea una falsa sensación de seguridad entre los desarrolladores. La verificación de tipos nos advierte cuando algo anda mal con nuestro código. Sin embargo, los tipos estáticos no reducen la densidad general de errores.
Por lo tanto, la fuerza de su programa dependerá de su uso de TypeScript, porque los tipos son escritos por el desarrollador y no se verifican en tiempo de ejecución.
Si está buscando TypeScript para reducir sus errores, considere el desarrollo basado en pruebas en su lugar.
Sistema de escritura complicado
El sistema de escritura, si bien es una gran herramienta en muchos aspectos, a veces puede ser un poco complicado. Esta desventaja se debe a que es totalmente interoperable con JavaScript, lo que deja aún más espacio para las complicaciones.
Sin embargo, TypeScript sigue siendo JavaScript, por lo que es importante comprender JavaScript.
¿Cuándo usar TypeScript?
Le aconsejaría que use TypeScript en los siguientes casos:
- Si está buscando crear una aplicación que se mantendrá durante un período prolongado , le recomiendo encarecidamente que comience con TypeScript, ya que fomenta el código de autodocumentación, lo que ayuda a otros desarrolladores a comprender su código fácilmente cuando se unen a su base de código. .
- Si necesita crear una biblioteca , considere escribirla en TypeScript. Ayudará a los editores de código a sugerir los tipos apropiados a los desarrolladores que usan su biblioteca.
En las últimas secciones, hemos equilibrado los pros y los contras de TypeScript. Pasemos al asunto del día: configurar TypeScript en un proyecto React moderno .
Empezando
Hay varias formas de configurar TypeScript en un proyecto de React. En este tutorial, cubriremos solo dos.
Método 1: crear la aplicación React + TypeScript
Hace aproximadamente dos años, el equipo de React lanzó Create React App 2.1, con soporte para TypeScript. Por lo tanto, es posible que nunca tenga que hacer ningún trabajo pesado para incluir TypeScript en su proyecto.
Para iniciar un nuevo proyecto Create React App, puede ejecutar esto...
npx create-react-app my-app --folder-name
… o esto:
yarn create react-app my-app --folder-name
Para agregar TypeScript a un proyecto Create React App, primero instálelo y sus respectivos @types
:
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
… o:
yarn add typescript @types/node @types/react @types/react-dom @types/jest
A continuación, cambie el nombre de los archivos (por ejemplo, index.js
a index.tsx
) y reinicie su servidor de desarrollo .
Eso fue rápido, ¿no?
Método 2: configurar TypeScript con Webpack
Webpack es un paquete de módulos estáticos para aplicaciones JavaScript. Toma todo el código de su aplicación y lo hace utilizable en un navegador web. Los módulos son fragmentos de código reutilizables creados a partir de los estilos JavaScript, node_modules
, imágenes y CSS de su aplicación, que se empaquetan para usarlos fácilmente en su sitio web.
Crear un nuevo proyecto
Comencemos creando un nuevo directorio para nuestro proyecto:
mkdir react-webpack cd react-webpack
Usaremos npm para inicializar nuestro proyecto:
npm init -y
El comando anterior generará un archivo package.json
con algunos valores predeterminados. Agreguemos también algunas dependencias para webpack, TypeScript y algunos módulos específicos de React.
Instalación de paquetes
Por último, necesitaríamos instalar los paquetes necesarios. Abra su interfaz de línea de comandos (CLI) y ejecute esto:
#Installing devDependencies npm install --save-dev @types/react @types/react-dom awesome-typescript-loader css-loader html-webpack-plugin mini-css-extract-plugin source-map-loader typescript webpack webpack-cli webpack-dev-server #installing Dependencies npm install react react-dom
También agreguemos manualmente algunos archivos y carpetas diferentes en nuestra carpeta react-webpack
:
- Agregue
webpack.config.js
para agregar configuraciones relacionadas con webpack. - Agregue
tsconfig.json
para todas nuestras configuraciones de TypeScript. - Agregue un nuevo directorio,
src
. - Cree un nuevo directorio,
components
, en la carpetasrc
. - Finalmente, agregue
index.html
,App.tsx
eindex.tsx
en la carpeta decomponents
.
Estructura del proyecto
Por lo tanto, nuestra estructura de carpetas se verá así:
├── package.json ├── package-lock.json ├── tsconfig.json ├── webpack.config.js ├── .gitignore └── src └──components ├── App.tsx ├── index.tsx ├── index.html
Comience a agregar algo de código
Empezaremos con index.html
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React-Webpack Setup</title> </head> <body> <div></div> </body> </html>
Esto creará el HTML, con un div
vacío con una ID de output
.
Agreguemos el código a nuestro componente React App.tsx
:
import * as React from "react"; export interface HelloWorldProps { userName: string; lang: string; } export const App = (props: HelloWorldProps) => ( <h1> Hi {props.userName} from React! Welcome to {props.lang}! </h1> );
Creamos un objeto de interfaz y lo HelloWorldProps
, con userName
y lang
con un tipo de string
.
Pasamos props
a nuestro componente de App
y lo exportamos.
Ahora, actualicemos el código en index.tsx
:
import * as React from "react"; import * as ReactDOM from "react-dom"; import { App } from "./App"; ReactDOM.render( <App userName="Beveloper" lang="TypeScript" />, document.getElementById("output") );
Acabamos de importar el componente de la App
en index.tsx
. Cuando webpack ve cualquier archivo con la extensión .ts
o .tsx
, transpilará ese archivo usando la biblioteca Awesome-TypeScript-Loader.
Configuración de mecanografiado
Luego agregaremos alguna configuración a tsconfig.json
:
{ "compilerOptions": { "jsx": "react", "module": "commonjs", "noImplicitAny": true, "outDir": "./build/", "preserveConstEnums": true, "removeComments": true, "sourceMap": true, "target": "es5" }, "include": [ "src/components/index.tsx" ] }
También veamos las diferentes opciones que agregamos a tsconfig.json
:
-
compilerOptions
Representa las diferentes opciones del compilador. -
jsx:react
Agrega soporte para JSX en archivos.tsx
. -
lib
Agrega una lista de archivos de biblioteca a la compilación (por ejemplo, usares2015
nos permite usar la sintaxis ECMAScript 6). -
module
Genera código de módulo. -
noImplicitAny
errores para declaraciones conany
tipo implícito. -
outDir
Representa el directorio de salida. -
sourceMap
Genera un archivo.map
, que puede ser muy útil para depurar la aplicación. -
target
Representa la versión de destino de ECMAScript para transpilar nuestro código (podemos agregar una versión según los requisitos específicos de nuestro navegador). -
include
Se utiliza para especificar la lista de archivos que se incluirán.
Configuración del paquete web
Agreguemos alguna configuración de paquete web a webpack.config.js
.
const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { entry: "./src/components/index.tsx", target: "web", mode: "development", output: { path: path.resolve(\__dirname, "build"), filename: "bundle.js", }, resolve: { extensions: [".js", ".jsx", ".json", ".ts", ".tsx"], }, module: { rules: [ { test: /\.(ts|tsx)$/, loader: "awesome-typescript-loader", }, { enforce: "pre", test: /\.js$/, loader: "source-map-loader", }, { test: /\.css$/, loader: "css-loader", }, ], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(\__dirname, "src", "components", "index.html"), }), new MiniCssExtractPlugin({ filename: "./src/yourfile.css", }), ], };
Veamos las diferentes opciones que hemos agregado a webpack.config.js
:
-
entry
Esto especifica el punto de entrada para nuestra aplicación. Puede ser un solo archivo o una matriz de archivos que queremos incluir en nuestra compilación. -
output
Contiene la configuración de salida. La aplicación analiza esto cuando intenta enviar el código incluido de nuestro proyecto al disco. La ruta representa el directorio de salida al que se enviará el código y el nombre de archivo representa el nombre de archivo del mismo. Generalmente se llamabundle.js
. -
resolve
Webpack analiza este atributo para decidir si agrupar u omitir el archivo. Por lo tanto, en nuestro proyecto, webpack considerará los archivos con las extensiones.js
,.jsx
,.json
,.ts
y.tsx
para agrupar. -
module
Podemos habilitar webpack para cargar un archivo en particular cuando lo solicite la aplicación, usando cargadores. Toma un objeto de reglas que especifica que:- cualquier archivo que termine con la extensión
.tsx
o.ts
debe usarawesome-typescript-loader
para ser cargado; - los archivos que terminan con la extensión
.js
deben cargarse consource-map-loader
; - los archivos que terminan con la extensión
.css
deben cargarse concss-loader
.
- cualquier archivo que termine con la extensión
-
plugins
Webpack tiene sus propias limitaciones y proporciona complementos para superarlas y ampliar sus capacidades. Por ejemplo,html-webpack-plugin
crea un archivo de plantilla que se representa en el navegador desde el archivoindex.html
en el directorio./src/component/index.html
.
MiniCssExtractPlugin
representa el archivo CSS
principal de la aplicación.
Agregar secuencias de comandos a package.json
Podemos agregar diferentes scripts para crear aplicaciones React en nuestro archivo package.json
:
"scripts": { "start": "webpack-dev-server --open", "build": "webpack" },
Ahora, ejecute npm start
en su CLI. Si todo salió bien, deberías ver esto:
Si tiene una habilidad especial para el paquete web, clone el repositorio para esta configuración y utilícelo en sus proyectos.
Creación de archivos
Cree una carpeta src
y un archivo index.tsx
. Este será el archivo base que renderiza React.
Ahora, si ejecutamos npm start
, ejecutará nuestro servidor y abrirá una nueva pestaña. Ejecutar npm run build
creará un paquete web para producción y creará una carpeta de compilación para nosotros.
Hemos visto cómo configurar TypeScript desde cero utilizando la aplicación Create React y el método de configuración del paquete web.
Una de las formas más rápidas de obtener una comprensión completa de TypeScript es convertir uno de sus proyectos React de vainilla existentes a TypeScript. Desafortunadamente, la adopción gradual de TypeScript en un proyecto Vanilla React existente es estresante porque implica tener que expulsar o cambiar el nombre de todos los archivos, lo que generaría conflictos y una solicitud de extracción gigante si el proyecto perteneciera a un equipo grande.
A continuación, veremos cómo migrar fácilmente un proyecto de React a TypeScript.
Migrar una aplicación Create React existente a TypeScript
Para que este proceso sea más manejable, lo dividiremos en pasos, lo que nos permitirá migrar en partes individuales. Estos son los pasos que seguiremos para migrar nuestro proyecto:
- Agregue TypeScript y tipos.
- Agregue
tsconfig.json
. - Empieza pequeño.
- Cambie el nombre de la extensión de los archivos a
.tsx
.
1. Agregue TypeScript al proyecto
Primero, necesitaremos agregar TypeScript a nuestro proyecto. Suponiendo que su proyecto React se haya iniciado con Create React App, podemos ejecutar lo siguiente:
# Using npm npm install --save typescript @types/node @types/react @types/react-dom @types/jest # Using Yarn yarn add typescript @types/node @types/react @types/react-dom @types/jest
Tenga en cuenta que todavía no hemos cambiado nada a TypeScript. Si ejecutamos el comando para iniciar el proyecto localmente ( npm start
o yarn start
), nada cambia. Si ese es el caso, ¡entonces genial! Estamos listos para el siguiente paso.
2. Agregue el archivo tsconfig.json
Antes de aprovechar TypeScript, debemos configurarlo a través del archivo tsconfig.json
. La forma más sencilla de comenzar es crear un andamio con este comando:
npx tsc --init
Esto nos da algunos conceptos básicos, con mucho código comentado. Ahora, reemplace todo el código en tsconfig.json
con esto:
{ "compilerOptions": { "jsx": "react", "module": "commonjs", "noImplicitAny": true, "outDir": "./build/", "preserveConstEnums": true, "removeComments": true, "sourceMap": true, "target": "es5" }, "include": [ "./src/**/**/\*" ] }
Configuración de mecanografiado
También veamos las diferentes opciones que agregamos a tsconfig.json
:
-
compilerOptions
Representa las diferentes opciones del compilador.-
target
Traduce construcciones de JavaScript más nuevas a una versión anterior, como ECMAScript 5. -
lib
Agrega una lista de archivos de biblioteca a la compilación (por ejemplo, usar es2015 nos permite usar la sintaxis ECMAScript 6). -
jsx:react
Agrega soporte para JSX en archivos.tsx
. -
lib
Agrega una lista de archivos de biblioteca a la compilación (por ejemplo, usar es2015 nos permite usar la sintaxis ECMAScript 6). -
module
Genera código de módulo. -
noImplicitAny
Se utiliza para generar errores para declaraciones conany
tipo implícito. -
outDir
Representa el directorio de salida. -
sourceMap
Genera un archivo.map
, que puede ser muy útil para depurar nuestra aplicación. -
include
Se utiliza para especificar la lista de archivos que se incluirán.
-
Las opciones de configuración variarán, según la demanda de un proyecto. Es posible que deba consultar la hoja de cálculo de opciones de TypeScript para averiguar qué encajaría con su proyecto.
Solo hemos tomado las medidas necesarias para preparar las cosas. Nuestro siguiente paso es migrar un archivo a TypeScript.
3. Comience con un componente simple
Aproveche la capacidad de TypeScript para adoptarse gradualmente. Vaya un archivo a la vez a su propio ritmo. Haz lo que tenga sentido para ti y tu equipo. No intentes abordarlo todo de una vez.
Para convertir correctamente esto, tenemos que hacer dos cosas:
- Cambie la extensión del archivo a
.tsx
. - Agregue la anotación de tipo (que requeriría algunos conocimientos de TypeScript).
4. Cambiar el nombre de las extensiones de archivo a .tsx
En una base de código grande, puede parecer agotador cambiar el nombre de los archivos individualmente.
Cambiar el nombre de múltiples archivos en macOS
Cambiar el nombre de varios archivos puede ser una pérdida de tiempo. Así es como puedes hacerlo en una Mac. Haga clic derecho (o Ctrl
+ clic, o haga clic con dos dedos simultáneamente en el panel táctil si está usando una MacBook) en la carpeta que contiene los archivos que desea cambiar de nombre. Luego, haga clic en "Mostrar en Finder". En el Finder, seleccione todos los archivos que desea cambiar de nombre. Haga clic con el botón derecho en los archivos seleccionados y elija "Renombrar elementos X..." Luego, verá algo como esto:
Inserte la cadena que desea encontrar y la cadena con la que desea reemplazar la cadena encontrada, y presione "Cambiar nombre". Hecho.
Cambiar el nombre de varios archivos en Windows
Cambiar el nombre de varios archivos en Windows está más allá del alcance de este tutorial, pero hay una guía completa disponible. Por lo general, obtendría errores después de cambiar el nombre de los archivos; solo necesita agregar las anotaciones de tipo. Puede repasar esto en la documentación.
Hemos cubierto cómo configurar TypeScript en una aplicación React. Ahora, construyamos una aplicación de selección de episodios para Money Heist usando TypeScript.
No cubriremos los tipos básicos de TypeScript. Es necesario revisar la documentación antes de continuar con este tutorial.
Hora de construir
Para que este proceso sea menos abrumador, lo dividiremos en pasos, lo que nos permitirá crear la aplicación en partes individuales. Estos son todos los pasos que seguiremos para crear el selector de episodios de La Casa de Papel:
- Scaffold una aplicación Create React.
- Obtener episodios.
- Cree los tipos e interfaces apropiados para nuestros episodios en
interface.ts
. - Configure la tienda para obtener episodios en
store.tsx
. - Cree la acción para obtener episodios en
action.ts
. - Cree un componente
EpisodeList.tsx
que contenga los episodios obtenidos. - Importe el componente
EpisodesList
a nuestra página de inicio usandoReact Lazy and Suspense
.
- Cree los tipos e interfaces apropiados para nuestros episodios en
- Añadir episodios.
- Configure la tienda para agregar episodios en
store.tsx
. - Cree la acción para agregar episodios en
action.ts
.
- Configure la tienda para agregar episodios en
- Eliminar episodios.
- Configure la tienda para eliminar episodios en
store.tsx
. - Cree la acción para eliminar episodios en
action.ts
.
- Configure la tienda para eliminar episodios en
- Episodio favorito.
- Importe el componente
EpisodesList
en el episodio favorito. - Render
EpisodesList
dentro del episodio favorito.
- Importe el componente
- Uso de Reach Router para la navegación.
configurar reaccionar
La forma más fácil de configurar React es usar Create React App. Create React App es una forma oficialmente admitida de crear aplicaciones React de una sola página. Ofrece una configuración de construcción moderna sin configuración.
Lo usaremos para iniciar la aplicación que construiremos. Desde su CLI, ejecute el siguiente comando:
npx create-react-app react-ts-app && cd react-ts-app
Una vez que la instalación sea exitosa, inicie el servidor React ejecutando npm start
.
Comprender las interfaces y los tipos en TypeScript
Las interfaces en TypeScript se usan cuando necesitamos dar tipos a las propiedades de los objetos. Por lo tanto, estaríamos usando interfaces para definir nuestros tipos.
interface Employee { name: string, role: string salary: number } const bestEmployee: Employee= { name: 'John Doe', role: 'IOS Developer', salary: '$8500' //notice we are using a string }
Al compilar el código anterior, veríamos este error: “Los tipos de salary
de propiedad son incompatibles. La string
de tipo no se puede asignar al number
de tipo ”.
Dichos errores ocurren en TypeScript cuando a una propiedad o variable se le asigna un tipo diferente al tipo definido. Específicamente, el fragmento anterior significa que a la propiedad de salary
se le asignó un tipo de string
en lugar de un tipo de number
.
Vamos a crear un archivo interface.ts
en nuestra carpeta src
. Copia y pega este código en él:
/** |-------------------------------------------------- | All the interfaces! |-------------------------------------------------- */ export interface IEpisode { airdate: string airstamp: string airtime: string id: number image: { medium: string; original: string } name: string number: number runtime: number season: number summary: string url: string } export interface IState { episodes: Array<IEpisode> favourites: Array<IEpisode> } export interface IAction { type: string payload: Array<IEpisode> | any } export type Dispatch = React.Dispatch<IAction> export type FavAction = ( state: IState, dispatch: Dispatch, episode: IEpisode ) => IAction export interface IEpisodeProps { episodes: Array<IEpisode> store: { state: IState; dispatch: Dispatch } toggleFavAction: FavAction favourites: Array<IEpisode> } export interface IProps { episodes: Array<IEpisode> store: { state: IState; dispatch: Dispatch } toggleFavAction: FavAction favourites: Array<IEpisode> }
Es una buena práctica agregar una "I" al nombre de la interfaz. Hace que el código sea legible. Sin embargo, puede decidir excluirlo.
Interfaz IEpisode
Nuestra API devuelve un conjunto de propiedades como fecha de airdate
, marca de airstamp
, airtime
de emisión, id
, image
, name
, number
, tiempo de runtime
, season
, summary
y url
. Por lo tanto, definimos una interfaz IEpisode
y establecimos los tipos de datos apropiados para las propiedades del objeto.
Interfaz de Estado
Nuestra interfaz IState
tiene propiedades de episodes
y favorites
, respectivamente, y una interfaz Array<IEpisode>
.
Acción
Las propiedades de la interfaz IAction
son payload
y type
. La propiedad de type
tiene un tipo de cadena, mientras que la carga útil tiene un tipo de Array | any
Array | any
Tenga en cuenta que Array | any
Array | any
significa una matriz de la interfaz del episodio o cualquier tipo.
El tipo de Dispatch
se establece en React.Dispatch
y una interfaz <IAction>
. Tenga en cuenta que React.Dispatch
es el tipo estándar para la función de dispatch
, de acuerdo con la base de código @types/react
, mientras que <IAction>
es una matriz de la acción de interfaz.
Además, Visual Studio Code tiene un comprobador de TypeScript. Entonces, simplemente resaltando o pasando el mouse sobre el código, es lo suficientemente inteligente como para sugerir el tipo apropiado.
En otras palabras, para que podamos hacer uso de nuestra interfaz en todas nuestras aplicaciones, debemos exportarla. Hasta ahora, tenemos nuestra tienda y nuestras interfaces que contienen el tipo de nuestro objeto. Ahora vamos a crear nuestra tienda. Tenga en cuenta que las otras interfaces siguen las mismas convenciones que las explicadas.
Obtener episodios
Crear una tienda
Para recuperar nuestros episodios, necesitamos un almacén que contenga el estado inicial de los datos y que defina nuestra función reductora.
Haremos uso del gancho useReducer
para configurar eso. Cree un archivo store.tsx
en su carpeta src
. Copie y pegue el siguiente código en él.
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext (initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
Los siguientes son los pasos que hemos seguido para crear la tienda:
- Al definir nuestra tienda, necesitamos el
useReducer
y la APIcreateContext
de React, por eso lo importamos. - Importamos
IState
eIAction
de./types/interfaces
. - Declaramos un objeto
initialState
con un tipo deIState
y propiedades de episodios y favoritos, ambos configurados en una matriz vacía, respectivamente. - A continuación, creamos una variable
Store
que contiene el métodocreateContext
y se le pasainitialState
.
El tipo de método createContext
es <IState | any>
<IState | any>
, lo que significa que podría ser un tipo de <IState>
o any
. Veremos el tipo any
usado a menudo en este artículo.
- A continuación, declaramos una función
reducer
y pasamosstate
yaction
como parámetros. La funciónreducer
tiene una declaración de cambio que verifica el valor deaction.type
. Si el valor esFETCH_DATA
, devuelve un objeto que tiene una copia de nuestro estado(...state)
y del estado del episodio que contiene nuestra carga útil de acción. - En la declaración de cambio, devolvemos un estado
default
.
Tenga en cuenta que los parámetros de state
y action
en la función reducer tienen tipos IState
e IAction
, respectivamente. Además, la función reducer
tiene un tipo de IState
.
- Por último, declaramos una función
StoreProvider
. Esto le dará a todos los componentes de nuestra aplicación acceso a la tienda. - Esta función toma a
children
como apoyo, y dentro de la funciónStorePrivder
, declaramos el ganchouseReducer
. - Desestructuramos
state
ydispatch
. - Para que nuestra tienda sea accesible para todos los componentes, pasamos un valor de objeto que contiene
state
ydispatch
.
El state
que contiene nuestros episodios y el estado de favoritos será accesible a través de otros componentes, mientras que el dispatch
es una función que cambia el estado.
- Exportaremos
Store
yStoreProvider
para que puedan usarse en nuestra aplicación.
Crear acción.ts
Tendremos que realizar solicitudes a la API para obtener los episodios que se mostrarán al usuario. Esto se hará en un archivo de acción. Cree un archivo Action.ts
y luego pegue el siguiente código:
import { Dispatch } from './interface/interfaces' export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON.\_embedded.episodes }) }
Primero, necesitamos importar nuestras interfaces para que puedan usarse en este archivo. Se siguieron los siguientes pasos para crear la acción:
- La función
fetchDataAction
toma props dedispatch
como parámetro. - Debido a que nuestra función es asíncrona,
async
yawait
. - Creamos una variable (
URL
) que contiene nuestro punto final de API. - Tenemos otra variable llamada
data
que contiene la respuesta de la API. - Luego, almacenamos la respuesta JSON en
dataJSON
, después de haber obtenido la respuesta en formato JSON llamando adata.json()
. - Por último, devolvemos una función de envío que tiene una propiedad de
type
y una cadena deFETCH_DATA
. También tiene unpayload()
._embedded.episodes
es la matriz del objeto de episodios de nuestroendpoint
.
Tenga en cuenta que la función fetchDataAction
obtiene nuestro punto final, lo convierte en objetos JSON
y devuelve la función de envío, que actualiza el estado declarado anteriormente en la Tienda.
El tipo de envío exportado se establece en React.Dispatch
. Tenga en cuenta que React.Dispatch
es el tipo estándar para la función de envío de acuerdo con la base de código @types/react
, mientras que <IAction>
es una matriz de la acción de la interfaz.
Componente EpisodesList
Para mantener la reutilización de nuestra aplicación, mantendremos todos los episodios obtenidos en un archivo separado y luego importaremos el archivo en nuestro componente de página de homePage
.
En la carpeta de components
, cree un archivo EpisodesList.tsx
y copie y pegue el siguiente código:
import React from 'react' import { IEpisode, IProps } from '../types/interfaces' const EpisodesList = (props: IProps): Array<JSX.Element> => { const { episodes } = props return episodes.map((episode: IEpisode) => { return ( <section key={episode.id} className='episode-box'> <img src={!!episode.image ? episode.image.medium : ''} alt={`Money Heist ${episode.name}`} /> <div>{episode.name}</div> <section style={{ display: 'flex', justifyContent: 'space-between' }}> <div> Season: {episode.season} Number: {episode.number} </div> <button type='button' > Fav </button> </section> </section> ) }) } export default EpisodesList
- Importamos
IEpisode
eIProps
desdeinterfaces.tsx
. - A continuación, creamos una función
EpisodesList
que toma accesorios. Los accesorios tendrán un tipo deIProps
, mientras que la función tiene un tipo deArray<JSX.Element>
.
Visual Studio Code sugiere que nuestro tipo de función se escriba como JSX.Element[]
.
Mientras que Array<JSX.Element>
es igual a JSX.Element[]
, Array<JSX.Element>
se denomina identidad genérica. Por lo tanto, el patrón genérico se usará con frecuencia en este artículo.
- Dentro de la función, desestructuramos los
episodes
deprops
, que tiene elIEpisode
como tipo.
Lea acerca de la identidad genérica. Este conocimiento será necesario a medida que avancemos.
- Devolvimos los accesorios de los
episodes
y los mapeamos para devolver algunas etiquetas HTML. - La primera sección contiene la
key
, que esepisode.id
, y un nombre declassName
deepisode-box
, que se creará más adelante. Sabemos que nuestros episodios tienen imágenes; por lo tanto, la etiqueta de la imagen. - La imagen tiene un operador ternario que verifica si hay un
episode.image
o unepisode.image.medium
. De lo contrario, mostramos una cadena vacía si no se encuentra ninguna imagen. Además, incluimos el nombre delepisode.name
en un div.
En la section
, mostramos la temporada a la que pertenece un episodio y su número. Tenemos un botón con el texto Fav
. Hemos exportado el componente EpisodesList
para que podamos usarlo en nuestra aplicación.
Componente de la página de inicio
Queremos que la página de inicio active la llamada API y muestre los episodios usando el componente EpisodesList
que creamos. Dentro de la carpeta de components
, cree el componente HomePage
y copie y pegue el siguiente código:
import React, { useContext, useEffect, lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces' import { fetchDataAction } from '../Actions' const EpisodesList = lazy<any>(() => import('./EpisodesList')) const HomePage = (): JSX.Element => { const { state, dispatch } = useContext(Store) useEffect(() => { state.episodes.length === 0 && fetchDataAction(dispatch) }) const props: IEpisodeProps = { episodes: state.episodes, store: { state, dispatch } } return ( <App> <Suspense fallback={<div>loading...</div>}> <section className='episode-layout'> <EpisodesList {...props} /> </section> </Suspense> </App> ) } export default HomePage
- Importamos
useContext
,useEffect
,lazy
ySuspense
de React. El componente de la aplicación importada es la base sobre la cual todos los demás componentes deben recibir el valor de la tienda. - También importamos
Store
,IEpisodeProps
yFetchDataAction
desde sus respectivos archivos. - Importamos el componente
EpisodesList
usando la funciónReact.lazy
disponible en React 16.6.
La carga diferida de React es compatible con la convención de división de código. Por lo tanto, nuestro componente EpisodesList
se carga dinámicamente, en lugar de cargarse de una vez, lo que mejora el rendimiento de nuestra aplicación.
- Desestructuramos el
state
y lodispatch
como accesorios de laStore
. - El ampersand (&&) en el gancho
useEffect
verifica si el estado de nuestros episodios estáempty
(o es igual a 0). De lo contrario, devolvemos la funciónfetchDataAction
. - Por último, devolvemos el componente
App
. Dentro de él, usamos el envoltorioSuspense
y establecemos elfallback
en un div con el texto deloading
. Esto se mostrará al usuario mientras esperamos la respuesta de la API. - El componente
EpisodesList
se montará cuando los datos estén disponibles, y los datos que contendrán losepisodes
son los que distribuimos en él.
Configurar Index.txs
El componente de la Homepage
de inicio debe ser un elemento secundario del StoreProvider
. Tendremos que hacer eso en el archivo index
. Cambie el nombre de index.js
a index.tsx
y pegue el siguiente código:
import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { StoreProvider } from './Store' import HomePage from './components/HomePage' ReactDOM.render( <StoreProvider> <HomePage /> </StoreProvider>, document.getElementById('root') )
Importamos StoreProvider
, HomePage
e index.css
desde sus respectivos archivos. We wrap the HomePage
component in our StoreProvider
. This makes it possible for the Homepage
component to access the store, as we saw in the previous section.
Hemos recorrido un largo camino. Let's check what the app looks like, without any CSS.
Create Index.css
Delete the code in the index.css
file and replace it with this:
html { font-size: 14px; } body { margin: 0; padding: 0; font-size: 10px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .episode-layout { display: flex; flex-wrap: wrap; min-width: 100vh; } .episode-box { padding: .5rem; } .header { display: flex; justify-content: space-between; background: white; border-bottom: 1px solid black; padding: .5rem; position: sticky; top: 0; }
Our app now has a look and feel. Here's how it looks with CSS.
Now we see that our episodes can finally be fetched and displayed, because we've adopted TypeScript all the way. Great, isn't it?
Add Favorite Episodes Feature
Let's add functionality that adds favorite episodes and that links it to a separate page. Let's go back to our Store component and add a few lines of code:
Note that the highlighted code is newly added:
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext<IState | any>(initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload }
case 'ADD_FAV': return { ...state, favourites: [...state.favourites, action.payload] }
default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return <Store.Provider value={{ state, dispatch }}>{children}</Store.Provider> }
To implement the “Add favorite” feature to our app, the ADD_FAV
case is added. It returns an object that holds a copy of our previous state, as well as an array with a copy of the favorite state
, with the payload
.
We need an action that will be called each time a user clicks on the FAV
button. Let's add the highlighted code to index.tx
:
import {
IAction, IEpisode, Dispatch } from './types/interfaces'
export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON._embedded.episodes }) }
export const toggleFavAction = (dispatch: any, episode: IEpisode | any): IAction => { let dispatchObj = { type: 'ADD_FAV', payload: episode } return dispatch(dispatchObj) }
export const toggleFavAction = (dispatch: any, episode: IEpisode | any): IAction => { let dispatchObj = { type: 'ADD_FAV', payload: episode } return dispatch(dispatchObj) }
We create a toggleFavAction
function that takes dispatch
and episodes
as parameters, and any
and IEpisode|any
as their respective types, with IAction
as our function type. We have an object whose type
is ADD_FAV
and that has episode
as its payload. Lastly, we just return and dispatch the object.
Agregaremos algunos fragmentos más a EpisodeList.tsx
. Copia y pega el código resaltado:
import React from 'react' import { IEpisode, IProps } from '../types/interfaces' const EpisodesList = (props: IProps): Array<JSX.Element> => {
const { episodes, toggleFavAction, favourites, store } = props const { state, dispatch } = store
return episodes.map((episode: IEpisode) => { return ( <section key={episode.id} className='episode-box'> <img src={!!episode.image ? episode.image.medium : ''} alt={`Money Heist - ${episode.name}`} /> <div>{episode.name}</div> <section style={{ display: 'flex', justifyContent: 'space-between' }}> <div> Seasion: {episode.season} Number: {episode.number} </div> <button type='button'
onClick={() => toggleFavAction(state, dispatch, episode)} > {favourites.find((fav: IEpisode) => fav.id === episode.id) ? 'Unfav' : 'Fav'}
</button> </section> </section> ) }) } export default EpisodesList
Incluimos togglefavaction
, favorites
y store
como accesorios, y desestructuramos state
, un dispatch
desde la tienda. Para seleccionar nuestro episodio favorito, incluimos el método toggleFavAction
en un evento onClick
y pasamos el state
, el dispatch
y los accesorios del episode
como argumentos a la función.
Por último, recorremos el estado favorite
para verificar si fav.id
(ID favorito) coincide con el episode.id
. Si es así, alternamos entre el texto Unfav
y Fav
. Esto ayuda al usuario a saber si ha marcado ese episodio como favorito o no.
Nos estamos acercando al final. Pero aún necesitamos una página a la que se puedan vincular los episodios favoritos cuando el usuario elija entre los episodios en la página de inicio.
Si has llegado hasta aquí, date una palmadita en la espalda.
Componente de página favorita
En la carpeta de components
, cree un archivo FavPage.tsx
. Copie y pegue el siguiente código en él:
import React, { lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces' import { toggleFavAction } from '../Actions' const EpisodesList = lazy<any>(() => import('./EpisodesList')) export default function FavPage(): JSX.Element { const { state, dispatch } = React.useContext(Store) const props: IEpisodeProps = { episodes: state.favourites, store: { state, dispatch }, toggleFavAction, favourites: state.favourites } return ( <App> <Suspense fallback={<div>loading...</div>}> <div className='episode-layout'> <EpisodesList {...props} /> </div> </Suspense> </App> ) }
Para crear la lógica detrás de la elección de episodios favoritos, hemos escrito un pequeño código. Importamos lazy
y Suspense
de React. También importamos Store
, IEpisodeProps
y toggleFavAction
desde sus respectivos archivos.
Importamos nuestro componente EpisodesList
utilizando la función React.lazy
. Por último, devolvemos el componente App
. Dentro de él, usamos el envoltorio Suspense
y establecemos un respaldo a un div con el texto de carga.
Esto funciona de manera similar al componente de la Homepage
de inicio. Este componente accederá a la tienda para obtener los episodios que el usuario haya marcado como favoritos. Luego, la lista de episodios se pasa al componente EpisodesList
.
Agreguemos algunos fragmentos más al archivo HomePage.tsx
.
Incluya el toggleFavAction
de ../Actions
. Incluya también el método toggleFavAction
como accesorios.
import React, { useContext, useEffect, lazy, Suspense } from 'react' import App from '../App' import { Store } from '../Store' import { IEpisodeProps } from '../types/interfaces'
import { fetchDataAction, toggleFavAction } from '../Actions'
const EpisodesList = lazy<any>(() => import('./EpisodesList')) const HomePage = (): JSX.Element => { const { state, dispatch } = useContext(Store) useEffect(() => { state.episodes.length === 0 && fetchDataAction(dispatch) }) const props: IEpisodeProps = { episodes: state.episodes, store: { state, dispatch },
toggleFavAction, favourites: state.favourites
} return ( <App> <Suspense fallback={<div>loading...</div>}> <section className='episode-layout'> <EpisodesList {...props} /> </section> </Suspense> </App> ) } export default HomePage
Nuestra FavPage
debe estar vinculada, por lo que necesitamos un enlace en nuestro encabezado en App.tsx
. Para lograr esto, usamos Reach Router, una biblioteca similar a React Router. William Le explica las diferencias entre Reach Router y React Router.
En su CLI, ejecute npm install @reach/router @types/reach__router
. Estamos instalando tanto la biblioteca Reach Router como los tipos reach-router
.
Tras una instalación exitosa, importe Link
desde @reach/router
.
import React, { useContext, Fragment } from 'react' import { Store } from './tsx'
import { Link } from '@reach/router'
const App = ({ children }: { children: JSX.Element }): JSX.Element => {
const { state } = useContext(Store)
return ( <Fragment> <header className='header'> <div> <h1>Money Heist</h1> <p>Pick your favourite episode</p> </div>
<div> <Link to='/'>Home</Link> <Link to='/faves'>Favourite(s): {state.favourites.length}</Link> </div>
</header> {children} </Fragment> ) } export default App
Desestructuramos la tienda desde useContext
. Por último, nuestra casa tendrá un Link
y una ruta a /
, mientras que nuestro favorito tendrá una ruta a /faves
.
{state.favourites.length}
comprueba el número de episodios en los estados favoritos y lo muestra.
Finalmente, en nuestro archivo index.tsx
, importamos los componentes FavPage
y HomePage
, respectivamente, y los envolvemos en el Router
.
Copie el código resaltado al código existente:
import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { StoreProvider } from './Store'
import { Router, RouteComponentProps } from '@reach/router' import HomePage from './components/HomePage' import FavPage from './components/FavPage' const RouterPage = ( props: { pageComponent: JSX.Element } & RouteComponentProps ) => props.pageComponent
ReactDOM.render( <StoreProvider>
<Router> <RouterPage pageComponent={<HomePage />} path='/' /> <RouterPage pageComponent={<FavPage />} path='/faves' /> </Router>
</StoreProvider>, document.getElementById('root') )
Ahora, veamos cómo funciona el ADD_FAV
implementado.
Eliminar funcionalidad favorita
Finalmente, agregaremos la función "Eliminar episodio", para que cuando se haga clic en el botón, alterne entre agregar o eliminar un episodio favorito. Mostraremos la cantidad de episodios agregados o eliminados en el encabezado.
TIENDA
Para crear la funcionalidad "Eliminar episodio favorito", agregaremos otro caso en nuestra tienda. Entonces, vaya a Store.tsx
y agregue el código resaltado:
import React, { useReducer, createContext } from 'react' import { IState, IAction } from './types/interfaces' const initialState: IState = { episodes: [], favourites: [] } export const Store = createContext<IState | any>(initialState) const reducer = (state: IState, action: IAction): IState => { switch (action.type) { case 'FETCH_DATA': return { ...state, episodes: action.payload } case 'ADD_FAV': return { ...state, favourites: [...state.favourites, action.payload] }
case 'REMOVE_FAV': return { ...state, favourites: action.payload }
default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
default: return state } } export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => { const [state, dispatch] = useReducer(reducer, initialState) return {children} }
Agregamos otro caso llamado REMOVE_FAV
y devolvemos un objeto que contiene la copia de nuestro initialState
. Además, el estado de favorites
contiene la carga útil de la acción.
ACCIÓN
Copie el siguiente código resaltado y péguelo en action.ts
:
import
{ IAction, IEpisode, IState, Dispatch } from './types/interfaces'
export const fetchDataAction = async (dispatch: Dispatch) => { const URL = 'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes' const data = await fetch(URL) const dataJSON = await data.json() return dispatch({ type: 'FETCH_DATA', payload: dataJSON.\_embedded.episodes }) } //Add IState withits type
export const toggleFavAction = (state: IState, dispatch: any, episode: IEpisode | any): IAction => { const episodeInFav = state.favourites.includes(episode)
let dispatchObj = { type: 'ADD_FAV', payload: episode }
if (episodeInFav) { const favWithoutEpisode = state.favourites.filter( (fav: IEpisode) => fav.id !== episode.id ) dispatchObj = { type: 'REMOVE_FAV', payload: favWithoutEpisode }
} return dispatch(dispatchObj) }
Importamos la interfaz IState
desde ./types/interfaces
, porque necesitaremos pasarla como el tipo a los accesorios de state
en la función toggleFavAction
.
Se episodeInFav
una variable episodioInFav para comprobar si existe un episodio en el estado de favorites
.
Filtramos a través del estado de favoritos para verificar si una identificación favorita no es igual a una identificación de episodio. Por lo tanto, a dispatchObj
se le reasigna un tipo de REMOVE_FAV
y una carga útil de favWithoutEpisode
.
Veamos una vista previa del resultado de nuestra aplicación.
Conclusión
En este artículo, hemos visto cómo configurar TypeScript en un proyecto de React y cómo migrar un proyecto de Vanilla React a TypeScript.
También creamos una aplicación con TypeScript y React para ver cómo se usa TypeScript en los proyectos de React. Confío en que hayas podido aprender algunas cosas.
Comparta sus comentarios y experiencias con TypeScript en la sección de comentarios a continuación. ¡Me encantaría ver qué se te ocurre!
El repositorio de apoyo para este artículo está disponible en GitHub.
Referencias
- “Cómo migrar una aplicación React a TypeScript”, Joe Previte
- "¿Por qué y cómo usar TypeScript en su aplicación React?", Mahesh Haldar