DEV Community

Cover image for React / Redux básico
Atauã Pinali Doederlein
Atauã Pinali Doederlein

Posted on • Originally published at notion.so

React / Redux básico

Chega um momento no estudo do React em que devemos enfrentar o Redux. Quando eu lidei com isso da primeira vez foi difícil e eu fiquei um tempo confuso, mas com o tempo a sua estrutura foi ficando mais clara, então eu decidi, como material de estudo, criar um esquema. Aos poucos vou melhorando esse esquema para torná-lo mais prático, e acredito que ele possa ajudar outros estudantes do assunto, assim resolvi publicá-lo aqui, seguido de explicações:

link para imagem mais visível

Para começar, é preciso importar as bibliotecas do Redux:

É só mandar um


yarn add redux react-redux redux-thunk redux-devtools-extension

Enter fullscreen mode Exit fullscreen mode

1.Estrutura de pastas

Uma boa estrutura inicial de pastas para o Redux seria a seguinte:


    redux
        ├── types
        │   └── index.js
        ├── actions
        │   └── index.js
        ├── thunk
        │   └── index.js
        ├── reducers
        │   └── index.js
        └── store
                └── index.js

Enter fullscreen mode Exit fullscreen mode

Essa é apenas uma proposta, e há quem não utilize a pasta store, preferindo utilizar o arquivo index.js direto da pasta redux, por exemplo, ou use os thunks junto das actions...

Faça a sua escolha! Busque a estrutura que lhe parecer mais lógica.

2.Responsabilidades

  • Types

Definição de variáveis. Usamos variáveis no lugar de strings para evitar erros de digitação, pois essas variáveis serão utilizadas diversas vezes em nossa aplicação.

// types/index.js -- ou types/products.js

export const SETLIST = "SET LIST";
export const RESET = "RESET";
Enter fullscreen mode Exit fullscreen mode
  • Actions

Funções criadoras de ações. Aqui são criados objetos que irão passar para o store o valor a ser alterado em nosso estado. Actions são funções puras, e tudo o que elas fazem é montar um objeto que será lido no reducer.

// actions/index.js -- ou actions/products.js

import { SETLIST, RESET } from '../types'

export const setList = ( newList ) => ({
    type = SETLIST,
    newList
})

export const reset = () => ({
    type = RESET,
})
Enter fullscreen mode Exit fullscreen mode
  • Thunk

Eu gosto de separar os thunks das actions, embora visualmente elas pareçam a mesma coisa. Aqui fica a lógica do que deve ser feito como efeito colateral antes de passar os dados para a action. Efeito colateral é o que altera dados fora do escopo da nossa função. Pode ser, por exemplo, a persistência de dados em um banco de dados ou em um arquivo csv.

No exemplo abaixo, a função addProduct recebe um produto por parâmetro, envia esse produto para o servidor, recebe como resposta a lista atualizada de produtos e envia essa lista atualizada para o reducer, utilizando a action setList (observe a diferença de responsabilidades entre actions e thunks)

// thunk/index.js -- ou thunk/products.js

import setList from '../actions'

export const addProduct = ( product ) => ( dispatch ) => {
    const url = ...
    const headers = { ... }
    axios.post(url, { product: product }, headers)
    .then(( res ) => {
        dispatch( setList( res.data.list ))
    })
    .catch(error => ...)
}
Enter fullscreen mode Exit fullscreen mode

NB: o axios trata os dados pelo formato correspondente; se for utilizar o fetch será preciso converter os dados para string e para json nas ocasiões apropriadas.

  • Reducers

Os reducers recebem os novos dados e atualizam o estado de maneira correspondente. É aqui que o estado é iniciado e fica armazenado. Normalmente, é “setado” um estado inicial vazio, que pode ser utilizado para posteriormente se limpar o estado.


// reducers/index.js -- ou reducers/products.js

import { SETLIST, RESET } from '../types'

const defaultList = []

const productList = (state = defaultState, action) => {
    const { type, newList = [] } = action

    switch type {

        case SETLIST:
            return newList

        case RESET:
            return defaultList

        default:
            return state
    }
}

export default productList
Enter fullscreen mode Exit fullscreen mode

Aqui é tomada uma ação de acordo com o valor recebido. No nosso exemplo, um novo produto é enviado para o thunk, que atualiza o back-end e retorna a lista atualizada. O thunk então despacha uma action com a nova lista, que é recebida no reducer , e é utilizada para atualizar o estado.

O novo estado vai assumir o valor retornado pelo switch

  • Store

A store recebe os valores do reducer e registra o estado de forma a torná-lo disponível em todos os lugares da aplicação.

// store/index.js

import { createStore, applyMiddleware } from "redux";

// opcional: esse item permite o debug do reducer no devtools do Chrome.
import { composeWithDevTools } from "redux-devtools-extension";

import thunk from "redux-thunk";

// o reducer **productList** exportado por default recebe outro nome no import:
import reducers from "../reducers";

const store = createStore(
  reducers,
  composeWithDevTools(applyMiddleware(thunk))
);

export default store;
Enter fullscreen mode Exit fullscreen mode

3.Estrutura mais complexa

Nosso exemplo acima é bem simples, mas aplicações volumosas podem ter várias actions, thunks e reducers, por exemplo para usuários (cadastro, login, alteração de dados...) e produtos no carrinho (adicionar, remover, limpar...) ou outros tipos de dados (mensagens entre usuários, chat com robô para tirar dúvidas, etc). Nesse caso, cada arquivo terá seus dados correspondentes, por exemplo:


    ├── reducers
    │  ├── index.js
    │  ├── users.js
    │  ├── products.js
    │  └── messages.js

Enter fullscreen mode Exit fullscreen mode

Nesse caso, seu index.js deve fazer a junção desses reducers (com as actions e thunks você não precisa se preocupar, pois eles são disponibilizados via import, então a estrutura de pastas não altera sua funcionalidade):

// reducers/index.js

import { combineReducers } from "redux";

// aqui importamos todos os reducers:
import users from "./users";
import products from "./products";
import messages from "./messages";

const store = combineReducers({ products, users, messages });
export default store;
Enter fullscreen mode Exit fullscreen mode

4.Pondo para funcionar

Para que a estrutura acima funcione, temos que disponibilizá-la globalmente em nossa aplicação. fazemos isso no arquivo App.jsx:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import store from "./redux/store";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
Enter fullscreen mode Exit fullscreen mode

O componente Provider recebe nossa store e a disponibiliza para tudo o que estiver contido dentro do componente App - isto é, toda nossa aplicação.

N.B.: Se sua aplicação tem uma estrutura simples, com apenas um reducer, você pode exportar seu único reducer e passá-lo para o Provider como valor de store, pulando o item 3 acima.

5.Usando e atualizando o estado no redux

Uma vez estruturado o estado no seu redux, agora você pode acessá-lo de qualquer lugar da sua aplicação, ou atualizá-lo, por meio dos hooks.


import { useSelector, useDispatch } from 'react-redux'
import { addProduct } from '../redux/thunk'
import { reset } from '../redux/actions'

const myComponent = () => {
    // vc tem que instanciar o useDispatch dentro do seu componente:
    const dispatch = useDispatch()

    // atualizar os dados
    const newProduct = {id: 5, name: 'flipflops', size: 45, gift: true }
    dispatch(addProduct(newProduct))

    // recuperar dados do redux
    const myList = useSelector(state => state.productList)

...

return(
    ...
    <button onClick={() => dispatch(reset())}>Limpar carrinho</button>
)

Enter fullscreen mode Exit fullscreen mode

Pronto! Nada de dados passando por props na lógica do nosso carrinho de compras.

O uso do Redux não nos priva de utilizarmos useState ou passar dados por props, mas agora esses recursos ficam circunscritos à lógica interna do componente. A parte pesada da manipulação do estado, com dados que precisam ser acessados de páginas e rotas distintas, é muito bem administrada pelo Redux.

Existem outros hooks e outras formas de fazer uma estrutura parecida com a descrita acima; a documentação do Redux pode ser consultada em https://redux.js.org/, e a do Thunk em https://www.npmjs.com/package/redux-thunk.

Para debugar o redux, eu inseri acima um código que permite a comunicação da sua aplicação com o redux dev tools, que funciona como uma extensão do navegador. Veja a documentação em https://github.com/zalmoxisus/redux-devtools-extension

Algumas considerações:

  • A estrutura acima é apenas uma proposta. Pode não ser a mais prática, mas eu tentei apresentar a estrutura mais lógica e enxuta possível - há quem prefira usar actions e thunks juntos na pasta das actions... isso pode concentrar melhor os conteúdos, mas há uma diferença entre o que cada elemento faz, e isso também determina de que maneira devemos procurar por erros quando alguma coisa quebra no nosso código. Se uma ação despachada da aplicação altera o estado, mas não atualiza o banco de dados, onde você vai procurar o erro? No local onde é feito o fetch, ou seja, em um thunk. Se é o contrário, atualiza o servidor mas não o estado local... bom, aí o problema pode estar em qualquer lugar a partir do retorno do fetch: no thunk, na action posterior ou no reducer que vai tratar essa action.

  • Também é comum ver as types sendo exportadas das actions. Mas, se você parar para pensar, são coisas distintas, e é bonito ver cada coisa no lugar certo.

  • A divisão das actions, reducers e types em pastas e arquivos separados se mostra mais eficaz quando um projeto grande é desenvolvido simultaneamente por várias pessoas. Cada uma delas, na sua branch, vai criar essas pastas, e na hora de fazer merge, dificilmente vai ter algum conflito, pois, no final, o que teremos serão arquivos separados. Pode haver conflito nos pontos de confluência, como no arquivo reducers/index.js, onde haverá o import dos reducers e eles terão que ser passados para o combineReducers. Mas a manutenção disso é muito mais fácil (a princípio é apenas uma linha) do que deixar tudo em um só lugar, inclusive no caso dos arquivos na pasta types.

# branch 1 -- products
redux
├── reducers
│  ├── products.js
│  └── index.js
├── actions
│  ├── products.js
│  └── index.js
...etc

# branch 2 -- users
redux
├── reducers
│  ├── users.js
│  └── index.js
├── actions
│  ├── users.js
│  └── index.js
...etc

# master, aṕos merge
redux
├── reducers
│  ├── products.js
│  ├── users.js
│  └── index.js
├── actions
│  ├── products.js
│  ├── users.js
│  └── index.js
...etc

Enter fullscreen mode Exit fullscreen mode

Na estrutura acima, o único conflito a ser tratado no merge será a linha que contém o export no index de cada pasta:

// reducers/indexs.js -- brach 1 (products)
import productList from './products.js'
...
const reducers = combineReducers({ productList });

// reducers/indexs.js -- brach 2 (users)
import userList from './users.js'
...
const reducers = combineReducers({ userList });

// reducers/indexs.js -- master após merge
import userList from './products.js'
import userList from './users.js'
...
const reducers = combineReducers({ productList, userList });
/* ^ a linha acima é a única que pode dar conflito
no momento do merge, facinho de dar manutenção */
Enter fullscreen mode Exit fullscreen mode

Top comments (0)