Configurando o TypeScript para projetos modernos do React usando o Webpack
Publicados: 2022-03-10Nesta 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).
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).
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.
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
:
- Adicione
webpack.config.js
para adicionar configurações relacionadas ao webpack. - Adicione
tsconfig.json
para todas as nossas configurações de TypeScript. - Adicione um novo diretório,
src
. - Crie um novo diretório,
components
, na pastasrc
. - Por fim, adicione
index.html
,App.tsx
eindex.tsx
na pasta decomponents
.
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, usares2015
nos permite usar a sintaxe ECMAScript 6). -
module
Gera o código do módulo. -
noImplicitAny
Gera erros para declarações comany
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 é chamadobundle.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 usarawesome-typescript-loader
para ser carregado; - os arquivos que terminam com a extensão
.js
devem ser carregados comsource-map-loader
; - os arquivos que terminam com a extensão
.css
devem ser carregados comcss-loader
.
- qualquer arquivo que termine com a extensão
-
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 arquivoindex.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:
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:
- Adicione TypeScript e tipos.
- Adicione
tsconfig.json
. - Comece pequeno.
- 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 comany
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:
- Altere a extensão do arquivo para
.tsx
. - 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:
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 usandoReact Lazy and Suspense
.
- Crie os tipos e interfaces apropriados para nossos episódios em
- Adicione episódios.
- Configure a loja para adicionar episódios em
store.tsx
. - Crie a ação para adicionar episódios em
action.ts
.
- Configure a loja para adicionar episódios em
- Remover episódios.
- Configure a loja para excluir episódios em
store.tsx
. - Crie a ação para excluir episódios em
action.ts
.
- Configure a loja para excluir episódios em
- Episódio favorito.
- Importar o componente
EpisodesList
no episódio favorito. - Render
EpisodesList
dentro do episódio favorito.
- Importar o componente
- 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
.
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 APIcreateContext
do React, e é por isso que a importamos. - Importamos
IState
eIAction
de./types/interfaces
. - Declaramos um objeto
initialState
com um tipo deIState
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étodocreateContext
e que recebe oinitialState
.
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 passamosstate
eaction
como parâmetros. A funçãoreducer
possui uma instrução switch que verifica o valor deaction.type
. Se o valor forFETCH_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çãoStorePrivder
, declaramos o hookuseReducer
. - Desestruturamos
state
edispatch
. - Para tornar nossa loja acessível a todos os componentes, passamos um valor de objeto contendo
state
edispatch
.
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
eStoreProvider
, 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 dedispatch
como parâmetro. - Como nossa função é assíncrona,
async
eawait
. - 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 chamandodata.json()
. - Por fim, retornamos uma função dispatch que possui uma propriedade do
type
e uma string deFETCH_DATA
. Ele também tem umpayload()
._embedded.episodes
é a matriz do objeto de episódios do nossoendpoint
.
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
eIProps
deinterfaces.tsx
. - Em seguida, criamos uma função
EpisodesList
que recebe props. As props terão um tipo deIProps
, enquanto a função terá um tipo deArray<JSX.Element>
.
O Visual Studio Code sugere que nosso tipo de função seja escrito como JSX.Element[]
.
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
deprops
, que tem oIEpisode
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 umclassName
deepisode-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 umepisode.image.medium
. Caso contrário, exibimos uma string vazia se nenhuma imagem for encontrada. Além disso, incluímos oepisode.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
eSuspense
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
eFetchDataAction
de seus respectivos arquivos. - Importamos o componente
EpisodesList
usando o recursoReact.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
edispatch
como adereços daStore
. - 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çãofetchDataAction
. - Por fim, retornamos o componente
App
. Dentro dele, usamos o wrapperSuspense
e definimos ofallback
para um div com o texto deloading
. 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 osepisodes
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.
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.
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.
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
- “Como migrar um aplicativo React para o TypeScript”, Joe Previte
- “Por que e como usar o TypeScript em seu aplicativo React?”, Mahesh Haldar