Configurando o TypeScript para projetos modernos do React usando o Webpack

Publicados: 2022-03-10
Resumo rápido ↬ Este artigo apresenta o Typescript, um sobrescrito de JavaScript que apresenta o recurso de tipo estático para detectar erros comuns como códigos de desenvolvedores, o que melhora o desempenho, resultando em aplicativos corporativos robustos. Você também aprenderá a configurar o TypeScript com eficiência em um projeto React à medida que construímos um aplicativo Money Heist Episode Picker, explorando TypeScript, ganchos React como useReducer, useContext e Reach Router.

Nesta era de desenvolvimento de software, o JavaScript pode ser usado para desenvolver quase qualquer tipo de aplicativo. No entanto, o fato de o JavaScript ser tipado dinamicamente pode ser uma preocupação para a maioria das grandes empresas, por causa de seu recurso de verificação de tipo solto.

Felizmente, não temos que esperar até que o Ecma Technical Committee 39 introduza um sistema de tipo estático em JavaScript. Podemos usar o TypeScript em vez disso.

JavaScript, sendo tipado dinamicamente, não está ciente do tipo de dados de uma variável até que essa variável seja instanciada em tempo de execução. Os desenvolvedores que escrevem grandes programas de software podem ter a tendência de reatribuir uma variável, declarada anteriormente, a um valor de um tipo diferente, sem nenhum aviso ou problema, resultando em bugs muitas vezes ignorados.

Neste tutorial, aprenderemos o que é o TypeScript e como trabalhar com ele em um projeto React. Ao final, teremos construído um projeto que consiste em um aplicativo de seleção de episódios para o programa de TV Money Heist , usando TypeScript e ganchos atuais do tipo React ( useState , useEffect , useReducer , useContext ). Com esse conhecimento, você pode experimentar o TypeScript em seus próprios projetos.

Este artigo não é uma introdução ao TypeScript. Portanto, não passaremos pela sintaxe básica do TypeScript e do JavaScript. No entanto, você não precisa ser um especialista em nenhuma dessas linguagens para acompanhar, porque tentaremos seguir o princípio KISS (simples, estúpido).

Mais depois do salto! Continue lendo abaixo ↓

O que é TypeScript?

Em 2019, o TypeScript foi classificado como a sétima linguagem mais usada e a quinta linguagem de crescimento mais rápido no GitHub. Mas o que exatamente é o TypeScript?

De acordo com a documentação oficial, TypeScript é um superconjunto tipado de JavaScript que compila para JavaScript simples. Ele é desenvolvido e mantido pela Microsoft e pela comunidade de código aberto.

“Superconjunto” neste contexto significa que a linguagem contém todos os recursos e funcionalidades do JavaScript e muito mais. TypeScript é uma linguagem de script tipada.

Ele oferece aos desenvolvedores mais controle sobre sua base de código por meio de anotação de tipo, classes e interface, poupando os desenvolvedores de ter que corrigir manualmente bugs irritantes no console.

O TypeScript não foi criado para alterar o JavaScript. Em vez disso, ele expande o JavaScript com novos recursos valiosos. Qualquer programa escrito em JavaScript simples também será executado conforme esperado no TypeScript, incluindo aplicativos móveis multiplataforma e back-ends em Node.js.

Isso significa que você também pode escrever aplicativos React no TypeScript, como faremos neste tutorial.

Por que TypeScript?

Talvez você não esteja convencido de abraçar a bondade do TypeScript. Vamos considerar algumas de suas vantagens.

Menos erros

Não podemos eliminar todos os bugs em nosso código, mas podemos reduzi-los. O TypeScript verifica os tipos em tempo de compilação e gera erros se o tipo de variável for alterado.

Ser capaz de encontrar esses erros óbvios, porém frequentes, logo no início torna muito mais fácil gerenciar seu código com tipos.

Refatorar é mais fácil

Você provavelmente quer refatorar muitas coisas, mas porque eles tocam em tantos outros códigos e muitos outros arquivos, você tem medo de modificá-los.

No TypeScript, essas coisas geralmente podem ser refatoradas com apenas um clique no comando “Renomear símbolo” em seu ambiente de desenvolvimento integrado (IDE).

Renomear aplicativo para expApp (visualização grande)

Em uma linguagem tipada dinamicamente como JavaScript, a única maneira de refatorar vários arquivos ao mesmo tempo é com a função tradicional “pesquisar e substituir” usando expressões regulares (RegExp).

Em uma linguagem de tipagem estática como o TypeScript, “pesquisar e substituir” não é mais necessário. Com comandos do IDE, como “Localizar todas as ocorrências” e “Renomear símbolo”, você pode ver todas as ocorrências no aplicativo de determinada função, classe ou propriedade de uma interface de objeto.

O TypeScript o ajudará a encontrar todas as instâncias do bit refatorado, renomeá-lo e alertá-lo com um erro de compilação caso seu código tenha algum tipo de incompatibilidade após a refatoração.

O TypeScript tem ainda mais vantagens do que abordamos aqui.

Desvantagens do TypeScript

O TypeScript certamente tem suas desvantagens, mesmo considerando os recursos promissores destacados acima.

Uma falsa sensação de segurança

O recurso de verificação de tipo do TypeScript geralmente cria uma falsa sensação de segurança entre os desenvolvedores. A verificação de tipos de fato nos avisa quando algo está errado com nosso código. No entanto, os tipos estáticos não reduzem a densidade geral de bugs.

Portanto, a força do seu programa dependerá do uso do TypeScript, porque os tipos são escritos pelo desenvolvedor e não verificados em tempo de execução.

Se você estiver procurando o TypeScript para reduzir seus bugs, considere o desenvolvimento orientado a testes.

Sistema de digitação complicado

O sistema de digitação, embora seja uma ótima ferramenta em muitos aspectos, às vezes pode ser um pouco complicado. Essa desvantagem decorre de ser totalmente interoperável com JavaScript, o que deixa ainda mais espaço para complicações.

No entanto, o TypeScript ainda é JavaScript, portanto, é importante entender o JavaScript.

Quando usar o TypeScript?

Eu aconselho você a usar o TypeScript nos seguintes casos:

  • Se você deseja criar um aplicativo que será mantido por um longo período , recomendo fortemente começar com o TypeScript, porque ele promove a autodocumentação do código, ajudando outros desenvolvedores a entender seu código facilmente quando ingressarem em sua base de código .
  • Se você precisar criar uma biblioteca , considere escrevê-la em TypeScript. Isso ajudará os editores de código a sugerir os tipos apropriados aos desenvolvedores que estão usando sua biblioteca.

Nas últimas seções, equilibramos os prós e contras do TypeScript. Vamos para o assunto do dia: configurar o TypeScript em um projeto React moderno .

Começando

Existem várias maneiras de configurar o TypeScript em um projeto React. Neste tutorial, abordaremos apenas dois.

Método 1: Criar React App + TypeScript

Cerca de dois anos atrás, a equipe do React lançou o Create React App 2.1, com suporte a TypeScript. Portanto, talvez você nunca precise fazer nenhum trabalho pesado para colocar o TypeScript em seu projeto.

Anúncio do TypeScript no Create React App (visualização grande)

Para iniciar um novo projeto Create React App, você pode executar este…

 npx create-react-app my-app --folder-name

… ou isto:

 yarn create react-app my-app --folder-name

Para adicionar TypeScript a um projeto Create React App, primeiro instale-o e seus respectivos @types :

 npm install --save typescript @types/node @types/react @types/react-dom @types/jest

… ou:

 yarn add typescript @types/node @types/react @types/react-dom @types/jest

Em seguida, renomeie os arquivos (por exemplo, index.js para index.tsx ) e reinicie seu servidor de desenvolvimento !

Isso foi rápido, não foi?

Método 2: configurar o TypeScript com o Webpack

Webpack é um empacotador de módulo estático para aplicativos JavaScript. Ele pega todo o código do seu aplicativo e o torna utilizável em um navegador da web. Os módulos são blocos reutilizáveis ​​de código criados a partir do JavaScript, node_modules , imagens e estilos CSS do seu aplicativo, que são empacotados para serem facilmente usados ​​em seu site.

Criar um novo projeto

Vamos começar criando um novo diretório para nosso projeto:

 mkdir react-webpack cd react-webpack

Usaremos o npm para inicializar nosso projeto:

 npm init -y

O comando acima irá gerar um arquivo package.json com alguns valores padrão. Vamos também adicionar algumas dependências para webpack, TypeScript e alguns módulos específicos do React.

Instalando Pacotes

Por fim, precisaríamos instalar os pacotes necessários. Abra sua interface de linha de comando (CLI) e execute isto:

 #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

Vamos também adicionar manualmente alguns arquivos e pastas diferentes em nossa pasta react-webpack :

  1. Adicione webpack.config.js para adicionar configurações relacionadas ao webpack.
  2. Adicione tsconfig.json para todas as nossas configurações de TypeScript.
  3. Adicione um novo diretório, src .
  4. Crie um novo diretório, components , na pasta src .
  5. Por fim, adicione index.html , App.tsx e index.tsx na pasta de components .

Estrutura do projeto

Assim, nossa estrutura de pastas ficará assim:

 ├── package.json ├── package-lock.json ├── tsconfig.json ├── webpack.config.js ├── .gitignore └── src └──components ├── App.tsx ├── index.tsx ├── index.html

Comece a adicionar algum código

Começaremos com 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>

Isso criará o HTML, com um div vazio com um ID de output .

Vamos adicionar o código ao nosso 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> );

Criamos um objeto de interface e o HelloWorldProps , com userName e lang tendo um tipo de string .

Passamos props para o nosso componente App e exportamos.

Agora, vamos atualizar o código em 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 o componente App para index.tsx . Quando o webpack vê qualquer arquivo com a extensão .ts ou .tsx , ele irá transpilar esse arquivo usando a biblioteca awesome-typescript-loader.

Configuração do TypeScript

Em seguida, adicionaremos algumas configurações ao tsconfig.json :

 { "compilerOptions": { "jsx": "react", "module": "commonjs", "noImplicitAny": true, "outDir": "./build/", "preserveConstEnums": true, "removeComments": true, "sourceMap": true, "target": "es5" }, "include": [ "src/components/index.tsx" ] }

Vejamos também as diferentes opções que adicionamos ao tsconfig.json :

  • compilerOptions Representa as diferentes opções do compilador.
  • jsx:react Adiciona suporte para JSX em arquivos .tsx .
  • lib Adiciona uma lista de arquivos de biblioteca à compilação (por exemplo, usar es2015 nos permite usar a sintaxe ECMAScript 6).
  • module Gera o código do módulo.
  • noImplicitAny Gera erros para declarações com any tipo implícito.
  • outDir Representa o diretório de saída.
  • sourceMap Gera um arquivo .map , que pode ser muito útil para depurar o aplicativo.
  • target Representa a versão do ECMAScript de destino para a qual transpilar nosso código (podemos adicionar uma versão com base em nossos requisitos específicos do navegador).
  • include Usado para especificar a lista de arquivos a ser incluída.

Configuração do Webpack

Vamos adicionar algumas configurações do webpack ao 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", }), ], };

Vejamos as diferentes opções que adicionamos ao webpack.config.js :

  • entry Isso especifica o ponto de entrada para nosso aplicativo. Pode ser um único arquivo ou uma matriz de arquivos que desejamos incluir em nossa compilação.
  • output Contém a configuração de saída. O aplicativo analisa isso ao tentar enviar código empacotado do nosso projeto para o disco. O caminho representa o diretório de saída para o código a ser enviado e o nome do arquivo representa o nome do arquivo para o mesmo. Geralmente é chamado bundle.js .
  • resolve Webpack examina esse atributo para decidir se agrupa ou ignora o arquivo. Assim, em nosso projeto, o webpack considerará arquivos com as extensões .js , .jsx , .json , .ts e .tsx para empacotamento.
  • module Podemos habilitar o webpack para carregar um determinado arquivo quando solicitado pelo aplicativo, usando loaders. É necessário um objeto de regras que especifica que:
    • qualquer arquivo que termine com a extensão .tsx ou .ts deve usar awesome-typescript-loader para ser carregado;
    • os arquivos que terminam com a extensão .js devem ser carregados com source-map-loader ;
    • os arquivos que terminam com a extensão .css devem ser carregados com css-loader .
  • plugins O Webpack tem suas próprias limitações e fornece plugins para superá-las e estender suas capacidades. Por exemplo, html-webpack-plugin cria um arquivo de modelo que é renderizado para o navegador a partir do arquivo index.html no diretório ./src/component/index.html .

MiniCssExtractPlugin renderiza o arquivo CSS pai do aplicativo.

Adicionando scripts ao package.json

Podemos adicionar scripts diferentes para construir aplicativos React em nosso arquivo package.json :

 "scripts": { "start": "webpack-dev-server --open", "build": "webpack" },

Agora, execute npm start em sua CLI. Se tudo correu bem, você deve ver isto:

Saída de configuração do React-Webpack (visualização grande)

Se você tem um talento especial para webpack, clone o repositório para esta configuração e use-o em seus projetos.

Criando arquivos

Crie uma pasta src e um arquivo index.tsx . Este será o arquivo base que renderiza o React.

Agora, se executarmos npm start , ele executará nosso servidor e abrirá uma nova guia. A execução npm run build compilará o webpack para produção e criará uma pasta de compilação para nós.

Vimos como configurar o TypeScript do zero usando o método de configuração Create React App e webpack.

Uma das maneiras mais rápidas de obter uma compreensão completa do TypeScript é convertendo um de seus projetos React existentes em TypeScript. Infelizmente, a adoção incremental do TypeScript em um projeto vanilla React existente é estressante porque envolve ter que ejetar ou renomear todos os arquivos, o que resultaria em conflitos e um pull request gigante se o projeto pertencesse a uma equipe grande.

Em seguida, veremos como migrar facilmente um projeto React para o TypeScript.

Migrar um aplicativo Create React existente para o TypeScript

Para tornar esse processo mais gerenciável, vamos dividi-lo em etapas, o que nos permitirá migrar em partes individuais. Aqui estão as etapas que seguiremos para migrar nosso projeto:

  1. Adicione TypeScript e tipos.
  2. Adicione tsconfig.json .
  3. Comece pequeno.
  4. Renomeie a extensão dos arquivos para .tsx .

1. Adicionar TypeScript ao projeto

Primeiro, precisaremos adicionar o TypeScript ao nosso projeto. Supondo que seu projeto React foi inicializado com Create React App, podemos executar o seguinte:

 # 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

Observe que ainda não alteramos nada no TypeScript. Se executarmos o comando para iniciar o projeto localmente ( npm start ou yarn start ), nada muda. Se for esse o caso, então ótimo! Estamos prontos para o próximo passo.

2. Adicione o arquivo tsconfig.json

Antes de aproveitar o TypeScript, precisamos configurá-lo através do arquivo tsconfig.json . A maneira mais simples de começar é criar um scaffold usando este comando:

 npx tsc --init

Isso nos dá algumas noções básicas, com muito código comentado. Agora, substitua todo o código em tsconfig.json por este:

 { "compilerOptions": { "jsx": "react", "module": "commonjs", "noImplicitAny": true, "outDir": "./build/", "preserveConstEnums": true, "removeComments": true, "sourceMap": true, "target": "es5" }, "include": [ "./src/**/**/\*" ] }

Configuração do TypeScript

Vejamos também as diferentes opções que adicionamos ao tsconfig.json :

  • compilerOptions Representa as diferentes opções do compilador.
    • target Traduz construções JavaScript mais recentes para uma versão mais antiga, como ECMAScript 5.
    • lib Adiciona uma lista de arquivos de biblioteca à compilação (por exemplo, usar es2015 nos permite usar a sintaxe ECMAScript 6).
    • jsx:react Adiciona suporte para JSX em arquivos .tsx .
    • lib Adiciona uma lista de arquivos de biblioteca à compilação (por exemplo, usar es2015 nos permite usar a sintaxe ECMAScript 6).
    • module Gera o código do módulo.
    • noImplicitAny Usado para gerar erros para declarações com any tipo implícito.
    • outDir Representa o diretório de saída.
    • sourceMap Gera um arquivo .map , que pode ser muito útil para depurar nosso aplicativo.
    • include Usado para especificar a lista de arquivos a ser incluída.

As opções de configurações variam de acordo com a demanda do projeto. Talvez seja necessário verificar a planilha de opções do TypeScript para descobrir o que se encaixa no seu projeto.

Tomamos apenas as medidas necessárias para deixar as coisas prontas. Nossa próxima etapa é migrar um arquivo para o TypeScript.

3. Comece com um componente simples

Aproveite a capacidade do TypeScript de ser adotado gradualmente. Vá um arquivo de cada vez no seu próprio ritmo. Faça o que faz sentido para você e sua equipe. Não tente resolver tudo de uma vez.

Para converter isso corretamente, precisamos fazer duas coisas:

  1. Altere a extensão do arquivo para .tsx .
  2. Adicione a anotação de tipo (o que exigiria algum conhecimento de TypeScript).

4.Renomeie as extensões de arquivo para .tsx

Em uma base de código grande, pode parecer cansativo renomear arquivos individualmente.

Renomeie vários arquivos no macOS

Renomear vários arquivos pode ser uma perda de tempo. Aqui está como você pode fazer isso em um Mac. Clique com o botão direito do mouse (ou Ctrl + clique ou clique com dois dedos simultaneamente no trackpad se estiver usando um MacBook) na pasta que contém os arquivos que deseja renomear. Em seguida, clique em “Revelar no Finder”. No Finder, selecione todos os arquivos que deseja renomear. Clique com o botão direito do mouse nos arquivos selecionados e escolha “Rename X items…” Então, você verá algo assim:

Renomear arquivos em um Mac (visualização grande)

Insira a string que você deseja encontrar e a string com a qual deseja substituir essa string encontrada e clique em “Renomear”. Feito.

Renomeie vários arquivos no Windows

Renomear vários arquivos no Windows está além do escopo deste tutorial, mas um guia completo está disponível. Você normalmente obteria erros após renomear os arquivos; você só precisa adicionar as anotações de tipo. Você pode retocar isso na documentação.

Cobrimos como configurar o TypeScript em um aplicativo React. Agora, vamos construir um aplicativo de seleção de episódios para Money Heist usando TypeScript.

Não abordaremos os tipos básicos de TypeScript. É necessário passar pela documentação antes de continuar neste tutorial.

Hora de construir

Para tornar esse processo menos assustador, dividiremos isso em etapas, o que nos permitirá criar o aplicativo em partes individuais. Aqui estão todos os passos que tomaremos para construir o seletor de episódios do Money Heist :

  • Scaffold um aplicativo Create React.
  • Buscar episódios.
    • Crie os tipos e interfaces apropriados para nossos episódios em interface.ts .
    • Configure a loja para buscar episódios em store.tsx .
    • Crie a ação para buscar episódios em action.ts .
    • Crie um componente EpisodeList.tsx que contém os episódios buscados.
    • Importe o componente EpisodesList para nossa página inicial usando React Lazy and Suspense .
  • Adicione episódios.
    • Configure a loja para adicionar episódios em store.tsx .
    • Crie a ação para adicionar episódios em action.ts .
  • Remover episódios.
    • Configure a loja para excluir episódios em store.tsx .
    • Crie a ação para excluir episódios em action.ts .
  • Episódio favorito.
    • Importar o componente EpisodesList no episódio favorito.
    • Render EpisodesList dentro do episódio favorito.
  • Usando o Reach Router para navegação.

Configurar o React

A maneira mais fácil de configurar o React é usar o Create React App. Create React App é uma maneira oficialmente suportada de criar aplicativos React de página única. Ele oferece uma configuração de compilação moderna sem configuração.

Vamos usá-lo para inicializar o aplicativo que vamos construir. Na sua CLI, execute o comando abaixo:

 npx create-react-app react-ts-app && cd react-ts-app

Quando a instalação for bem-sucedida, inicie o servidor React executando npm start .

Página inicial do React (visualização grande)

Entendendo interfaces e tipos no Typescript

Interfaces em TypeScript são usadas quando precisamos dar tipos a propriedades de objetos. Portanto, estaríamos usando interfaces para definir nossos 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 }

Ao compilar o código acima, veríamos este erro: “Tipos de salary de propriedade são incompatíveis. O tipo string não pode ser atribuído ao tipo number .”

Esses erros ocorrem no TypeScript quando uma propriedade ou variável recebe um tipo diferente do tipo definido. Especificamente, o trecho acima significa que a propriedade de salary foi atribuída a um tipo de string em vez de um tipo de number .

Vamos criar um arquivo interface.ts em nossa pasta src . Copie e cole este código nele:

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

É uma boa prática adicionar um “I” ao nome da interface. Isso torna o código legível. No entanto, você pode decidir excluí-lo.

Interface de episódio IE

Nossa API retorna um conjunto de propriedades como airdate , airstamp , airtime , id , image , name , number , runtime , season , summary e url . Assim, definimos uma interface IEpisode e configuramos os tipos de dados apropriados para as propriedades do objeto.

I Interface de estado

Nossa interface IState possui propriedades de episodes e favorites , respectivamente, e uma interface Array<IEpisode> .

IAção

As propriedades da interface IAction são payload e type . A propriedade type tem um tipo string, enquanto a carga útil tem um tipo Array | any Array | any .

Observe que Array | any Array | any significa uma matriz da interface do episódio ou qualquer tipo.

O tipo Dispatch é definido como React.Dispatch e uma interface <IAction> . Observe que React.Dispatch é o tipo padrão para a função dispatch , de acordo com a base de código @types/react react, enquanto <IAction> é um array da ação Interface.

Além disso, o Visual Studio Code tem um verificador de TypeScript. Portanto, apenas destacando ou passando o mouse sobre o código, é inteligente o suficiente para sugerir o tipo apropriado.

Em outras palavras, para usarmos nossa interface em nossos aplicativos, precisamos exportá-la. Até agora, temos nossa loja e nossas interfaces que guardam o tipo de nosso objeto. Vamos agora criar nossa loja. Observe que as outras interfaces seguem as mesmas convenções explicadas.

Buscar episódios

Criando uma loja

Para buscar nossos episódios, precisamos de um armazenamento que contenha o estado inicial dos dados e que defina nossa função redutora.

Usaremos o gancho useReducer para configurar isso. Crie um arquivo store.tsx em sua pasta src . Copie e cole o seguinte código nele.

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

A seguir estão as etapas que tomamos para criar a loja:

  • Ao definir nossa loja, precisamos do gancho useReducer e da API createContext do React, e é por isso que a importamos.
  • Importamos IState e IAction de ./types/interfaces .
  • Declaramos um objeto initialState com um tipo de IState e propriedades de episódios e favoritos, que são ambos definidos como um array vazio, respectivamente.
  • Em seguida, criamos uma variável Store que contém o método createContext e que recebe o initialState .

O tipo de método createContext é <IState | any> <IState | any> , o que significa que pode ser um tipo de <IState> ou any . Veremos o tipo any usado com frequência neste artigo.

  • Em seguida, declaramos uma função reducer e passamos state e action como parâmetros. A função reducer possui uma instrução switch que verifica o valor de action.type . Se o valor for FETCH_DATA , ele retornará um objeto que possui uma cópia do nosso estado (...state) e do estado do episódio que contém nossa carga útil de ação.
  • Na instrução switch, retornamos um estado default .

Observe que os parâmetros de state e action na função redutora têm os tipos IState e IAction , respectivamente. Além disso, a função reducer tem um tipo de IState .

  • Por fim, declaramos uma função StoreProvider . Isso dará a todos os componentes do nosso aplicativo acesso à loja.
  • Esta função recebe os children como prop, e dentro da função StorePrivder , declaramos o hook useReducer .
  • Desestruturamos state e dispatch .
  • Para tornar nossa loja acessível a todos os componentes, passamos um valor de objeto contendo state e dispatch .

O state que contém nossos episódios e o estado de favoritos será acessível por outros componentes, enquanto o dispatch é uma função que altera o estado.

  • Exportaremos Store e StoreProvider , para que possam ser usados ​​em nosso aplicativo.

Criar Action.ts

Precisaremos fazer solicitações à API para buscar os episódios que serão mostrados ao usuário. Isso será feito em um arquivo de ação. Crie um arquivo Action.ts e cole o seguinte 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 }) }

Primeiro, precisamos importar nossas interfaces para que possam ser usadas neste arquivo. As seguintes etapas foram realizadas para criar a ação:

  • A função fetchDataAction usa props de dispatch como parâmetro.
  • Como nossa função é assíncrona, async e await .
  • Criamos uma variável ( URL ) que contém nosso endpoint de API.
  • Temos outra variável chamada data que contém a resposta da API.
  • Em seguida, armazenamos a resposta JSON em dataJSON , depois de obtermos a resposta no formato JSON chamando data.json() .
  • Por fim, retornamos uma função dispatch que possui uma propriedade do type e uma string de FETCH_DATA . Ele também tem um payload() . _embedded.episodes é a matriz do objeto de episódios do nosso endpoint .

Observe que a função fetchDataAction busca nosso endpoint, converte-o em objetos JSON e retorna a função dispatch, que atualiza o estado declarado anteriormente na Store.

O tipo de despacho exportado é definido como React.Dispatch . Observe que React.Dispatch é o tipo padrão para a função dispatch de acordo com a base de código @types/react react, enquanto <IAction> é um array da Interface Action.

Componente de lista de episódios

Para manter a capacidade de reutilização de nosso aplicativo, manteremos todos os episódios buscados em um arquivo separado e, em seguida, importaremos o arquivo em nosso componente homePage .

Na pasta de components , crie um arquivo EpisodesList.tsx e copie e cole o seguinte código nele:

 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 e IProps de interfaces.tsx .
  • Em seguida, criamos uma função EpisodesList que recebe props. As props terão um tipo de IProps , enquanto a função terá um tipo de Array<JSX.Element> .

O Visual Studio Code sugere que nosso tipo de função seja escrito como JSX.Element[] .

O Visual Studio Code sugere um tipo (visualização grande)

Enquanto Array<JSX.Element> é igual a JSX.Element[] , Array<JSX.Element> é chamado de identidade genérica. Portanto, o padrão genérico será usado com frequência neste artigo.

  • Dentro da função, desestruturamos os episodes de props , que tem o IEpisode como tipo.

Leia sobre a identidade genérica, Este conhecimento será necessário à medida que prosseguirmos.

  • Devolvemos os adereços dos episodes e mapeamos para retornar algumas tags HTML.
  • A primeira seção contém a key , que é episode.id , e um className de episode-box , que será criado posteriormente. Sabemos que nossos episódios têm imagens; daí, a marca de imagem.
  • A imagem tem um operador ternário que verifica se há um episode.image ou um episode.image.medium . Caso contrário, exibimos uma string vazia se nenhuma imagem for encontrada. Além disso, incluímos o episode.name em uma div.

Na section , mostramos a temporada a que pertence um episódio e seu número. Temos um botão com o texto Fav . Exportamos o componente EpisodesList para que possamos usá-lo em nosso aplicativo.

Componente da página inicial

Queremos que a página inicial acione a chamada da API e exiba os episódios usando o componente EpisodesList que criamos. Dentro da pasta de components , crie o componente HomePage e copie e cole o seguinte código nele:

 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 e Suspense do React. O componente importado do aplicativo é a base sobre a qual todos os outros componentes devem receber o valor da loja.
  • Também importamos Store , IEpisodeProps e FetchDataAction de seus respectivos arquivos.
  • Importamos o componente EpisodesList usando o recurso React.lazy disponível no React 16.6.

O carregamento lento do React suporta a convenção de divisão de código. Assim, nosso componente EpisodesList é carregado dinamicamente, em vez de ser carregado de uma só vez, melhorando assim o desempenho do nosso aplicativo.

  • Desestruturamos o state e dispatch como adereços da Store .
  • O e comercial (&&) no gancho useEffect verifica se nosso estado de episódios está empty (ou igual a 0). Caso contrário, retornamos a função fetchDataAction .
  • Por fim, retornamos o componente App . Dentro dele, usamos o wrapper Suspense e definimos o fallback para um div com o texto de loading . Isso será exibido para o usuário enquanto aguardamos a resposta da API.
  • O componente EpisodesList será montado quando os dados estiverem disponíveis, e os dados que conterão os episodes são os que disseminamos nele.

Configurar Index.txs

O componente Homepage precisa ser filho do StoreProvider . Teremos que fazer isso no arquivo index . Renomeie index.js para index.tsx e cole o seguinte 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 de seus respectivos arquivos. 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.

Percorremos um longo caminho. Let's check what the app looks like, without any CSS.

App without CSS (Large preview)

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.

(Visualização grande)

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.

Adicionaremos mais alguns trechos ao EpisodeList.tsx . Copie e cole o código destacado:

 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

Incluímos togglefavaction , favorites e store como props e desestruturamos state , um dispatch da store. Para selecionar nosso episódio favorito, incluímos o método toggleFavAction em um evento onClick e passamos os props state , dispatch e episode como argumentos para a função.

Por fim, percorremos o estado favorite para verificar se fav.id (ID favorito) corresponde ao episode.id . Se isso acontecer, alternamos entre o texto Unfav e Fav . Isso ajuda o usuário a saber se ele marcou esse episódio como favorito ou não.

Estamos chegando perto do fim. Mas ainda precisamos de uma página onde os episódios favoritos possam ser vinculados quando o usuário escolher entre os episódios na página inicial.

Se você chegou até aqui, dê um tapinha nas costas.

Componente de página favorita

Na pasta de components , crie um arquivo FavPage.tsx . Copie e cole o seguinte código nele:

 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 criar a lógica por trás da escolha de episódios favoritos, escrevemos um pequeno código. Importamos lazy e Suspense do React. Também importamos Store , IEpisodeProps e toggleFavAction de seus respectivos arquivos.

Importamos nosso componente EpisodesList usando o recurso React.lazy . Por fim, retornamos o componente App . Dentro dele, usamos o wrapper Suspense e definimos um fallback para um div com o texto de carregamento.

Isso funciona de forma semelhante ao componente Homepage . Este componente acessará a loja para obter os episódios favoritos do usuário. Em seguida, a lista de episódios é passada para o componente EpisodesList .

Vamos adicionar mais alguns trechos ao arquivo HomePage.tsx .

Inclua o toggleFavAction de ../Actions . Inclua também o método toggleFavAction como props.

 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

Nossa FavPage precisa estar vinculada, então precisamos de um link em nosso cabeçalho em App.tsx . Para isso, usamos o Reach Router, uma biblioteca semelhante ao React Router. William Le explica as diferenças entre o Reach Router e o React Router.

Em sua CLI, execute npm install @reach/router @types/reach__router . Estamos instalando a biblioteca do roteador reach-router .

Após a instalação bem-sucedida, importe o Link de @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

Desestruturamos a loja de useContext . Por fim, nossa home terá um Link e um caminho para / , enquanto nosso favorito terá um caminho para /faves .

{state.favourites.length} verifica o número de episódios nos estados favoritos e o exibe.

Finalmente, em nosso arquivo index.tsx , importamos os componentes FavPage e HomePage , respectivamente, e os envolvemos no Router .

Copie o código destacado para o 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') )

Agora, vamos ver como funciona o ADD_FAV implementado.

O código "Adicionar favorito" funciona (visualização grande)

Remover funcionalidade favorita

Por fim, adicionaremos o recurso “Remover episódio”, para que, ao clicar no botão, alternemos entre adicionar ou remover um episódio favorito. Vamos exibir o número de episódios adicionados ou removidos no cabeçalho.

ARMAZENAR

Para criar a funcionalidade “Remover episódio favorito”, adicionaremos outro caso em nossa loja. Então, vá até Store.tsx e adicione o código destacado:

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

Adicionamos mais um caso chamado REMOVE_FAV e retornamos um objeto contendo a cópia de nosso initialState . Além disso, o estado de favorites contém a carga útil da ação.

AÇAO

Copie o seguinte código destacado e cole-o em 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 a interface IState de ./types/interfaces , porque precisaremos passá-la como o tipo para as props de state na função toggleFavAction .

Uma variável episodeInFav é criada para verificar se há um episódio que existe no estado de favorites .

Filtramos o estado de favoritos para verificar se um ID de favorito não é igual a um ID de episódio. Assim, o dispatchObj é reatribuído a um tipo de REMOVE_FAV e uma carga útil de favWithoutEpisode .

Vamos visualizar o resultado do nosso aplicativo.

Conclusão

Neste artigo, vimos como configurar o TypeScript em um projeto React e como migrar um projeto do vanilla React para o TypeScript.

Também criamos um aplicativo com TypeScript e React para ver como o TypeScript é usado em projetos React. Eu acredito que você foi capaz de aprender algumas coisas.

Por favor, compartilhe seus comentários e experiências com o TypeScript na seção de comentários abaixo. Eu adoraria ver o que você inventa!

O repositório de suporte para este artigo está disponível no GitHub.

Referências

  1. “Como migrar um aplicativo React para o TypeScript”, Joe Previte
  2. “Por que e como usar o TypeScript em seu aplicativo React?”, Mahesh Haldar