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:
Para começar, é preciso importar as bibliotecas do Redux:
É só mandar um
yarn add redux react-redux redux-thunk redux-devtools-extension
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
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";
- 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,
})
- 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 => ...)
}
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
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;
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
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;
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")
);
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>
)
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
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 */
Top comments (0)