Artigo original: How to Use Redux in Your React TypeScript App

O Redux é um contêiner de estado previsível para aplicações escritas em JavaScript. Ele também é popularmente conhecido como uma biblioteca para o gerenciamento de estado em aplicações do React.

Utilizar o Redux junto com TypeScript oferece ao desenvolvedor uma melhor experiência, pois o TypeScript é um superconjunto do JavaScript que verifica o código para torná-lo robusto e compreensível.

Neste guia, mostrarei como usar o Redux em seu projeto com TypeScript criando uma aplicação do React que permite adicionar, excluir e exibir artigos.

Vamos lá!

Pré-requisitos

Este tutorial pressupõe que você tenha pelo menos um conhecimento básico de React, Redux e TypeScript.

Portanto, se não estiver familiarizado com essas tecnologias, tente primeiro ler este guia prático do TypeScript ou este tutorial do React Redux (textos em inglês). Caso contrário, vamos começar.

Configurando o projeto

Para usar o Redux e o TypeScript, precisamos criar uma aplicação em React.

Para isso, vamos abrir a CLI (interface de linha de comando) e executar este comando:

  npx create-react-app my-app --template typescript

A seguir, vamos estruturar o projeto do seguinte modo:

├── src
|  ├── components
|  |  ├── AddArticle.tsx
|  |  └── Article.tsx
|  ├── store
|  |  ├── actionCreators.ts
|  |  ├── actionTypes.ts
|  |  └── reducer.ts
|  ├── type.d.ts
|  ├── App.test.tsx
|  ├── App.tsx
|  ├── index.css
|  ├── index.tsx
|  ├── react-app-env.d.ts
|  └── setupTests.ts
├── tsconfig.json
├── package.json
└── yarn.lock

A estrutura de arquivos do projeto é bastante simples. No entanto, há duas coisas que você deve prestar atenção:

  • A pasta store do repositório, que contém arquivos relacionados ao React Redux.
  • O arquivo type.d.ts, que contém os tipos do TypeScript, que podem ser usados agora em outros arquivos sem importar.

Dito isso, agora, podemos instalar o Redux e criar nosso primeiro repositório.

Com isso, vamos abrir o projeto e executar o seguinte comando:

  yarn add redux react-redux redux-thunk

Se, no lugar do Yarn, você estiver usando o npm, execute:

  npm install redux react-redux redux-thunk

Também precisamos instalar os tipos como dependências de desenvolvimento para ajudar o TypeScript a entender as bibliotecas.

Então, vamos executar este comando novamente na CLI.

  yarn add -D @types/redux @types/react-redux @types/redux-thunk

No npm:

  npm install -D @types/redux @types/react-redux @types/redux-thunk

Ótimo! A partir de agora podemos criar os tipos do TypeScript para o projeto na próxima seção.

Criando os tipos

Os tipos do TypeScript permitem definir tipos para suas variáveis, parâmetros de função e assim por diante.

  • type.d.ts
interface IArticle {
  id: number
  title: string
  body: string
}

type ArticleState = {
  articles: IArticle[]
}

type ArticleAction = {
  type: string
  article: IArticle
}

type DispatchType = (args: ArticleAction) => ArticleAction

Aqui, começamos declarando a interface IArticle, que reflete a forma de um determinado artigo.

Em seguida, temos ArticleState, ArticleAction e DispatchType, que servirão como tipos para, respectivamente, o objeto de estado, os criadores de ação e a função de dispatch fornecida pelo Redux.

Dito isso, agora temos os tipos necessários para começar a usar o React Redux. A seguir, vamos criar os tipos de ação.

Criando os tipos de ação

  • store/actionTypes.ts
export const ADD_ARTICLE = "ADD_ARTICLE"
export const REMOVE_ARTICLE = "REMOVE_ARTICLE"

Precisamos de dois tipos de ação para o repositório em Redux, um para adicionar artigos e outro para excluí-los.

Criando os criadores de ação

  • store/actionCreators.ts
import * as actionTypes from "./actionTypes"

export function addArticle(article: IArticle) {
  const action: ArticleAction = {
    type: actionTypes.ADD_ARTICLE,
    article,
  }

  return simulateHttpRequest(action)
}

export function removeArticle(article: IArticle) {
  const action: ArticleAction = {
    type: actionTypes.REMOVE_ARTICLE,
    article,
  }
  return simulateHttpRequest(action)
}

export function simulateHttpRequest(action: ArticleAction) {
  return (dispatch: DispatchType) => {
    setTimeout(() => {
      dispatch(action)
    }, 500)
  }
}

Neste tutorial, simularei a solicitação HTTP atrasando-a por meio segundo. Se quiser, porém, sinta-se à vontade para usar um servidor real.

Aqui, a função addArticle fará o dispatching de uma ação para adicionar um novo artigo, enquanto o método removeArticle fará o oposto. Portanto, exclua o objeto passado como argumento.

Criando um redutor

Um redutor (em inglês, reducer) é uma função pura que recebe o estado do repositório e uma ação como parâmetro e depois retorna o estado atualizado.

  • store/reducer.ts
import * as actionTypes from "./actionTypes"

const initialState: ArticleState = {
  articles: [
    {
      id: 1,
      title: "post 1",
      body:
        "Quisque cursus, metus vitae pharetra Nam libero tempore, cum soluta nobis est eligendi",
    },
    {
      id: 2,
      title: "post 2",
      body:
        "Harum quidem rerum facilis est et expedita distinctio quas molestias excepturi sint",
    },
  ],
}

Como você pode ver aqui, declaramos um estado inicial para que alguns artigos sejam exibidos quando a página for carregada. O objeto de estado precisa corresponder ao tipo ArticleState – caso contrário, o TypeScript gerará um erro.

  • store/reducer.ts
const reducer = (
  state: ArticleState = initialState,
  action: ArticleAction
): ArticleState => {
  switch (action.type) {
    case actionTypes.ADD_ARTICLE:
      const newArticle: IArticle = {
        id: Math.random(), // not really unique
        title: action.article.title,
        body: action.article.body,
      }
      return {
        ...state,
        articles: state.articles.concat(newArticle),
      }
    case actionTypes.REMOVE_ARTICLE:
      const updatedArticles: IArticle[] = state.articles.filter(
        article => article.id !== action.article.id
      )
      return {
        ...state,
        articles: updatedArticles,
      }
  }
  return state
}

export default reducer

Em seguida, temos a função reducer, que espera o estado anterior e uma ação para poder atualizar o repositório. Aqui, temos duas ações: uma para adicionar e outra para excluir.

Com isso definido, agora podemos manipular o estado com o redutor. Próximo passo: criar um repositório para o projeto.

Criando um repositório

O repositório em Redux é, na prática, onde vive o estado da sua aplicação.

  • index.tsx
import * as React from "react"
import { render } from "react-dom"
import { createStore, applyMiddleware, Store } from "redux"
import { Provider } from "react-redux"
import thunk from "redux-thunk"

import App from "./App"
import reducer from "./store/reducer"

const store: Store<ArticleState, ArticleAction> & {
  dispatch: DispatchType
} = createStore(reducer, applyMiddleware(thunk))

const rootElement = document.getElementById("root")
render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)

Como você pode ver, importamos a função do redutor e a passamos como argumento para o método createStore para criar um repositório do Redux. O middleware redux-thunk precisa ser passado como um segundo parâmetro, bem como o método para poder manipular o código assíncrono.

Em seguida, conectamos o React ao Redux fornecendo o objeto store como props ao componente Provider.

Agora, podemos usar o Redux neste projeto e acessar o repositório. Então, vamos criar os componentes para obter e manipular os dados.

Criando os componentes

  • components/AddArticle.tsx
import * as React from "react"

type Props = {
  saveArticle: (article: IArticle | any) => void
}

export const AddArticle: React.FC<Props> = ({ saveArticle }) => {
  const [article, setArticle] = React.useState<IArticle | {}>()

  const handleArticleData = (e: React.FormEvent<HTMLInputElement>) => {
    setArticle({
      ...article,
      [e.currentTarget.id]: e.currentTarget.value,
    })
  }

  const addNewArticle = (e: React.FormEvent) => {
    e.preventDefault()
    saveArticle(article)
  }

  return (
    <form onSubmit={addNewArticle} className="Add-article">
      <input
        type="text"
        id="title"
        placeholder="Title"
        onChange={handleArticleData}
      />
      <input
        type="text"
        id="body"
        placeholder="Description"
        onChange={handleArticleData}
      />
      <button disabled={article === undefined ? true : false}>
        Add article
      </button>
    </form>
  )
}

Para adicionar um novo artigo, usaremos este componente de formulário. Ele recebe como parâmetro a função saveArticle, que permite adicionar um novo artigo ao repositório.

O objeto do artigo deve seguir o tipo IArticle para satisfazer os critérios TypeScript.

  • components/Article.tsx
import * as React from "react"
import { Dispatch } from "redux"
import { useDispatch } from "react-redux"

type Props = {
  article: IArticle
  removeArticle: (article: IArticle) => void
}

export const Article: React.FC<Props> = ({ article, removeArticle }) => {
  const dispatch: Dispatch<any> = useDispatch()

  const deleteArticle = React.useCallback(
    (article: IArticle) => dispatch(removeArticle(article)),
    [dispatch, removeArticle]
  )

  return (
    <div className="Article">
      <div>
        <h1>{article.title}</h1>
        <p>{article.body}</p>
      </div>
      <button onClick={() => deleteArticle(article)}>Delete</button>
    </div>
  )
}

O componente Article mostra um objeto de artigo.

A função removeArticle deve fazer o dispatching para acessar o repositório e, em seguida, excluir um determinado artigo. Essa é a razão pela qual usamos o hook useDispatch aqui, que permite que o Redux complete a ação de remoção.

Por conseguinte, o uso de useCallback ajuda a evitar renderizações desnecessárias ao memorizar valores como dependências.

Finalmente, temos os componentes que precisamos para adicionar e mostrar os artigos. Agora, vamos adicionar a última peça ao quebra-cabeça, usando tudo isso no arquivo App.tsx.

  • App.tsx
import * as React from "react"
import { useSelector, shallowEqual, useDispatch } from "react-redux"
import "./styles.css"

import { Article } from "./components/Article"
import { AddArticle } from "./components/AddArticle"
import { addArticle, removeArticle } from "./store/actionCreators"
import { Dispatch } from "redux"

const App: React.FC = () => {
  const articles: readonly IArticle[] = useSelector(
    (state: ArticleState) => state.articles,
    shallowEqual
  )

  const dispatch: Dispatch<any> = useDispatch()

  const saveArticle = React.useCallback(
    (article: IArticle) => dispatch(addArticle(article)),
    [dispatch]
  )

  return (
    <main>
      <h1>My Articles</h1>
      <AddArticle saveArticle={saveArticle} />
      {articles.map((article: IArticle) => (
        <Article
          key={article.id}
          article={article}
          removeArticle={removeArticle}
        />
      ))}
    </main>
  )
}

export default App

O hook useSelector permite o acesso ao estado do repositório. Aqui, passamos o shallowEqual como um segundo argumento para o método, de modo a dizer ao Redux para usar a igualdade rasa (shallow equality, em inglês) ao verificar as alterações.

Em seguida, contamos com useDispatch para fazer o dispatching de uma ação com o propósito de adicionar artigos no repositório. Por fim, percorremos o array de artigos e passamos cada um deles para o componente Article para que seja mostrado.

Com isso, podemos navegar até a raiz do projeto e executar este comando:

  yarn start

No npm:

  npm start

Se você abrir http://localhost:3000/ no navegador, deverá ver isto:

mxuc7kv9gtkiuuxdf4hx

Ótimo! Nossa aplicação está funcionando. Com isso, terminamos de mostrar o uso do Redux em uma aplicação do React com TypeScript.

Você pode encontrar o projeto completo nesta CodeSandbox.

Você pode encontrar outros conteúdos excelentes como este no blog do autor ou segui-lo no Twitter para ser notificado sobre novos textos produzidos por ele.

Obrigado pela leitura.