Criando Actions dentro dos Reducers
Anteriormente, você aprendeu a criar seus reducers
por meio dos arquivos que chamamos de slice.js
. Lá nós vimos como acessar cada objeto que declaramos dentro dos nossos componentes.
Agora chegou o momento de criar as nossas actions
, fazendo com que consigamos salvar informações dentro do nosso objeto que está presente em nosso reducer
.
Mas antes de continuar, vamos configurar o nosso projeto inicial 😉
Criando seu projeto de testes
Antes de começarmos, vamos criar um novo projeto em ReactJS dedicado a esta lição. No meu caso criei um novo projeto chamado de actions-com-reducer
:
npx create-react-app actions-com-reducer
Não se esqueça de instalar também as bibliotecas necessárias:
npm install redux react-redux @reduxjs/toolkit
Após isso, eu vou aproveitar aquela pasta do src que desenvolvemos no projeto anterior. Vou fazer isso pra gente ganhar um pouco de tempo no desenvolvimento dessa lição 😉
Criando nossas actions
Aproveitando o gancho da aula anterior, vamos criar nossa primeira action
dentro do reducer
de usuário
, da seguinte forma:
redux > user > slice.js
:
import { createSlice } from '@reduxjs/toolkit';
const InitialState = {
user: {
id: 1,
name: 'Micilini Roll',
site: 'https://micilini.com/'
}
}
export const userSlice = createSlice({
name: 'user',
initialState: InitialState,
reducers: {
createUser: (state, action) => {
//O State é o estado atual do reducer (objeto initialState). O action é o payload que passamos ao chamar a action. Lembrando que o payload nada mais é do que os dados que passamos para a action por meio dos nossos componentes.
console.log(state.user);//Mostra o estado atual do usuário (objeto initialState).
console.log(action.payload);//Mostra os resultados que estão no payload.
return {
...state,
user: action.payload
}
}
}
});
export const { createUser } = userSlice.actions;//Precisamos exportar as nossas actions para serem usadas dentro dos nossos componentes
export default userSlice.reducer;
No código acima nós criamos uma nova action
chamada de createUser
que recebe o state
que nada mais é do que o estado atual do reducer
(objeto initialState
) e o action
, que é o payload
que passamos ao chamar a action
.
Lembrando que o payload
nada mais é do que os dados que passamos para a action
por meio dos nossos componentes.
E falando em payload
, porque não passar chamar essa nossa action
dentro do componente <Perfil />
?
Perfil > index.jsx
:
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { createUser } from "../../redux/user/slice";
const Perfil = () => {
const { user } = useSelector((rootReducer) => rootReducer.user);
const dispatch = useDispatch();
const handleAction = () => {
dispatch(createUser({
id: 2,
name: 'Micilini Roll 2',
site: 'https://micilini.com/2'
}));
}
return(
<>
<h1>Meu Perfil</h1>
<p>Id: {user.id}</p>
<p>Nome: {user.name}</p>
<p>Site: {user.site}</p>
<button onClick={handleAction}>Chamar Action 1</button>
</>
);
}
export default Perfil;
No código acima estamos importante a função useDispatch
do Redux, que é responsável por executar a lógica da chamada de nossas actions
.
Já dentro da função handleAction()
, estamos chamando o createUser
(nossa action
que declaramos anteriormente) por meio da constante dispatch
(que tem o useDispatch
executado).
Por fim, estamos passando novos dados que serão armazenados dentro o objeto initialState
, que são:
{
id: 2,
name: 'Micilini Roll 2',
site: 'https://micilini.com/2'
}
Veja como ficou o resultado final:
Basicamente, quando clicamos no botão "Chamar Action 1", o ReactJS chama a função handleAction
que por sua vez, executa a nossa action createUser
que declaramos no nosso reducer
.
Que em seguida, recebe os novos parâmetros via payload
, e que serão enviados via return
da seguinte forma:
return {
...state,
user: action.payload
}
No comando acima é retornado uma cópia do initialState
(que existe dentro de state
), já com os dados alterados (user: action.payload
).
Aqui, um novo objeto de estado é criado usando o spread operator (...state
). Isso cria uma cópia superficial do estado existente, permitindo que você atualize propriedades específicas do estado sem modificar o estado original.
Ou seja, isso significa que o estado original de initialState
ainda permanece como:
{
id: 1,
name: 'Micilini Roll',
site: 'https://micilini.com/'
}
Mas como o return
é capaz de atualizar o store
, ele sofre alteração, e isso reflete em toda sua aplicação, e é por isso que o id
, name
e site
tem seus valores alterados dentro do componente Perfil
.
Beleza, mas será que é possível mudar o estado atual de forma definitiva?
Sim, observe esta nova action
:
redux > user > slice.js
:
import { createSlice } from '@reduxjs/toolkit';
const InitialState = {
user: {
id: 1,
name: 'Micilini Roll',
site: 'https://micilini.com/'
}
}
export const userSlice = createSlice({
name: 'user',
initialState: InitialState,//podemos passar de forma direta o initialState sem atribuir a uma variável
reducers: {
createUser: (state, action) => {
//O State é o estado atual do reducer (objeto initialState). O action é o payload que passamos ao chamar a action. Lembrando que o payload nada mais é do que os dados que passamos para a action por meio dos nossos componentes.
console.log(state.user);//Mostra o estado atual do usuário (objeto initialState)
console.log(action.payload);//Mostra os resultados que estão no payload
return {
...state,
user: action.payload
}
},
createUserSecond: (state, action) => {
console.log(state.user);//Mostra o estado atual do usuário (objeto initialState)
console.log(action.payload);//Mostra os resultados que estão no payload
state.user = action.payload;//Aqui estamos alterando o estado do reducer. Estamos dizendo que o estado do usuário é igual ao payload que passamos ao chamar a action createUser.
//Observação, quando modificamos nossa draft state, o Redux Toolkit faz uma verificação de imutabilidade para nós. Ou seja, não precisamos nos preocupar em retornar um novo objeto, o Redux Toolkit faz isso para nós. (Não usar o return aqui corrige o erro: 'Error: An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft')
}
}
});
export const { createUser, createUserSecond } = userSlice.actions;//Precisamos exportar as nossas actions para serem usadas dentro dos nossos componentes
export default userSlice.reducer;
No caso do createUserSecond
, a atualização do estado é feita diretamente modificando o estado existente (state.user = action.payload
).
Este método não cria um novo objeto state
, mas sim modifica o estado atual, e isso é conhecido como uma mutação de estado.
Observação: Se você executar um return
alí dentro, pode ocasionar o erro "Error: An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft"
, que sempre acontece quando tentamos retornar um estado no redux que já foi modificado.
Para solucionar este erro, nunca execute um return
dentro de uma action
na qual já teve seu estado alterado (state.user = action.payload
).
Veja como ficou o nosso componente Perfil
atualizado.
Perfil > index.jsx
:
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { createUser, createUserSecond } from "../../redux/user/slice";
const Perfil = () => {
const { user } = useSelector((rootReducer) => rootReducer.user);
const dispatch = useDispatch();
const handleAction = () => {
dispatch(createUser({
id: 2,
name: 'Micilini Roll 2',
site: 'https://micilini.com/2'
}));
}
const handleAction2 = () => {
dispatch(createUserSecond({
id: 3,
name: 'Micilini Roll 3',
site: 'https://micilini.com/3'
}));
}
return(
<>
<h1>Meu Perfil</h1>
<p>Id: {user.id}</p>
<p>Nome: {user.name}</p>
<p>Site: {user.site}</p>
<button onClick={handleAction}>Chamar Action 1</button>
<button onClick={handleAction2}>Chamar Action 2</button>
</>
);
}
export default Perfil;
Veja como ficou o resultado final:
Lembrando que você pode modificar apenas uma ou algumas chave do seu initialState
da seguinte forma:
return {
...state,
user.id: action.payload.id,
user.name: action.payload.name
}
No caso do exemplo acima modificamos apenas as chaves id
e name
de initialState
, mantendo o site
conforme seu valor original 🙂
Apagando estados por meio de Actions
Supondo que você queira apagar todos aqueles dados que estão armazenados dentro do initialState
:
user: {
id: 1,
name: 'Micilini Roll',
site: 'https://micilini.com/'
}
Você pode simplesmente criar uma nova action
capaz de fazer isso da seguinte forma:
deleteUser: (state) => {
state.user = null;
},
deleteUserSecond: (state) => {
return{
...state,
user: null
}
}
Lembrando que o deleteUser
remove o estado original transformando ele em nulo (mutação), já o deleteUserSecond
não remove seu estado original, mas deixa o novo estado vazio (nulo).
Veja como ficou o arquivo final.
redux > user > index.jsx
:
import { createSlice } from '@reduxjs/toolkit';
const InitialState = {
user: {
id: 1,
name: 'Micilini Roll',
site: 'https://micilini.com/'
}
}
export const userSlice = createSlice({
name: 'user',
initialState: InitialState,//podemos passar de forma direta o initialState sem atribuir a uma variável
reducers: {
createUser: (state, action) => {
//O State é o estado atual do reducer (objeto initialState). O action é o payload que passamos ao chamar a action. Lembrando que o payload nada mais é do que os dados que passamos para a action por meio dos nossos componentes.
console.log(state.user);//Mostra o estado atual do usuário (objeto initialState)
console.log(action.payload);//Mostra os resultados que estão no payload
return {
...state,
user: action.payload
}
},
createUserSecond: (state, action) => {
console.log(state.user);//Mostra o estado atual do usuário (objeto initialState)
console.log(action.payload);//Mostra os resultados que estão no payload
state.user = action.payload;//Aqui estamos alterando o estado do reducer. Estamos dizendo que o estado do usuário é igual ao payload que passamos ao chamar a action createUser.
//Observação, quando modificamos nossa draft state, o Redux Toolkit faz uma verificação de imutabilidade para nós. Ou seja, não precisamos nos preocupar em retornar um novo objeto, o Redux Toolkit faz isso para nós. (Não usar o return aqui corrige o erro: 'Error: An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft')
},
deleteUser: (state) => {
state.user = null;
},
deleteUserSecond: (state) => {
return{
...state,
user: null
}
}
}
});
export const { createUser, createUserSecond, deleteUser, deleteUserSecond } = userSlice.actions;//Precisamos exportar as nossas actions para serem usadas dentro dos nossos componentes
export default userSlice.reducer;
Não se esqueça de chamar as funções deleteUser
ou deleteUserSecond
dentro do seu componente Perfil
🤓
Perfil > index.jsx
:
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { createUser, createUserSecond, deleteUser, deleteUserSecond } from "../../redux/user/slice";
const Perfil = () => {
const { user } = useSelector((rootReducer) => rootReducer.user);
const dispatch = useDispatch();
const handleAction = () => {
dispatch(createUser({
id: 2,
name: 'Micilini Roll 2',
site: 'https://micilini.com/2'
}));
}
const handleAction2 = () => {
dispatch(createUserSecond({
id: 3,
name: 'Micilini Roll 3',
site: 'https://micilini.com/3'
}));
}
const handleDelete = () => {
dispatch(deleteUser());
}
const handleDelete2 = () => {
dispatch(deleteUserSecond());
}
return(
<>
<h1>Meu Perfil</h1>
<p>Id: {user?.id}</p>
<p>Nome: {user?.name}</p>
<p>Site: {user?.site}</p>
<button onClick={handleAction}>Chamar Action 1</button>
<button onClick={handleAction2}>Chamar Action 2</button>
<button onClick={handleDelete}>Deletar Usuário 1</button>
<button onClick={handleDelete2}>Deletar Usuário 2</button>
</>
);
}
export default Perfil;
No caso do deleteUse
e também do deleteUserSecond
, note que ele não recebe nenhuma action
e nenhum payload
, sendo assim, basta chamá-los da seguinte forma:
dispatch(deleteUser());
dispatch(deleteUserSecond());
Veja como ficou o resultado final:
Realizando renderizações condicionais dentro de nossas actions
Também é possível fazer renderizações condicionais dentro de nossa actions.
Vamos pegar como exemplo a action
createUser
:
createUser: (state, action) => {
if(action.payload.id === 1){
alert('O SEU ID NÃO PODE SER 1!');
return {...state};
}
console.log(state.user);//Mostra o estado atual do usuário (objeto initialState)
console.log(action.payload);//Mostra os resultados que estão no payload
return {
...state,
user: action.payload
}
},
No exemplo acima nós estamos verificando se o id
do payload
é igual a 1
, e se for, ele mostra um alerta no navegador do usuário informando que não foi possível atualizar o initialState
e retorna os dados sem atualização (return {...state}
).
Só que... retornar uma mensagem de alerta é muito feio, não é verdade? Sendo assim como eu faria para retornar uma mensagem de erro padrão?
Da seguinte forma:
createUser: (state, action) => {
if (action.payload.id === 1) {
return {
...state,
errorMessage: 'O SEU ID NÃO PODE SER 1!'
};
}
console.log(state.user); // Mostra o estado atual do usuário (objeto initialState)
console.log(action.payload); // Mostra os resultados que estão no payload
return {
...state,
user: action.payload,
errorMessage: null // Limpa o erro caso tenha sido exibido anteriormente
};
},
Observe que estamos retornando uma nova chave chamada de errorMessage
. Para pegá-la dentro do seu componente, basta fazer algo como:
import React from 'react';
import { useSelector } from 'react-redux';
const UserComponent = () => {
const user = useSelector(state => state.user);
const errorMessage = useSelector(state => state.errorMessage);
const handleCreateUser = () => {
// Lógica para chamar a ação que dispara o reducer 'createUser'
};
return (
<div>
{errorMessage && (
<div className="error-message">{errorMessage}</div>
)}
<button onClick={handleCreateUser}>Criar Usuário</button>
<div>
<pre>{JSON.stringify(user, null, 2)}</pre>
</div>
</div>
);
};
export default UserComponent;
Note que criamos uma variável chamada errorMessage
que é especialista em pegar mensagens de erros 😋
Além disso, você também pode fazer verificações não só nos payloads
como também no próprio estado da seguinte forma:
createUser: (state, action) => {
if(state.user === null){
return {
...state,
errorMessage: 'O usuário está vazio!'
};
}
if (action.payload.id === 1) {
return {
...state,
errorMessage: 'O SEU ID NÃO PODE SER 1!'
};
}
console.log(state.user); // Mostra o estado atual do usuário (objeto initialState)
console.log(action.payload); // Mostra os resultados que estão no payload
return {
...state,
user: action.payload,
errorMessage: null // Limpa o erro caso tenha sido exibido anteriormente
};
},
Interessante, não acha?
Adicionando novos objetos dentro do initialState
Também é possível adicionar novos objetos dentro do nosso initialState
além daqueles que já existem por lá.
Vamos pegar como exemplo o nosso initialState
de user
que armazena o segunte objeto:
const InitialState = {
user: {
id: 1,
name: 'Micilini Roll',
site: 'https://micilini.com/'
}
}
Supondo que você queira adicionar mais uma chave chamada de rank
dentro de user
, você poderia criar uma action
capaz de fazer isso da seguinte forma:
addRankUser: (state, action) => {
return {
...state,
user: {
...state.user, // mantém as propriedades existentes do usuário
rank: action.payload.rank // adiciona a nova propriedade rank
}
};
}
Para chamar essa action
no componente, é só seguir as dicas anteriores 😁
Resetando um estado para um valor padrão
Com o Redux também podemos resetar um determinado estado para que ele volte a ter seu valor padrão, e o procedimento é bem simples:
resetUser: (state) => {
// Reseta o usuário para o estado inicial
state.user = initialState.user;
}
Separação das Actions
Nos tópicos anteriores, você deve ter percebido que sempre utilizamos o padrão camelCase
para nomear as nossas actions
, que no caso, é o padrão recomendado pelo Redux.
Além disso, no final de cada action
, sempre inserimos uma referência ao nome do nosso reducer, que é user
:
Apesar do createUserSecond
e deleteUserSecond
estarem com o termo User
no meio da palavra, o certo seria createSecondUser
e deleteSecondUser
, mantendo o termo User
sempre no final. Isso é uma boa prática!
Uma outra boa prática é sempre separar nossas actions por funcionalidade, por exemplo:
createUser
: só serve para criar um usuário.
deleteUser
: só serve para remover um usuário.
updateUser
: só serve para atualizar o usuário.
updateAddressUser
: só serve para atualizar o endereço do usuário.
addRankUser
: só serve para adicionar um rank no usuário.
Por fim, sempre que for modificar o seu initialState
sempre prefira fazer isso usando o return
, em vez de uma atribuição direta (mutação):
return {
...state,
user: action.payload
}
Onde está o type?
Em lições passadas, nós demos uma olhada nessa ilustração abaixo:
Alí, nós aprendemos que cada action
existente no Redux, na verdade se trata de um objeto que contém o type
e "outras chaves" a mais.
Com relação às "outras chaves", você aprendeu a fazer a manipulação de cada um delas...
return {
...state,
user: action.payload//O user em sí é considerado "outras chaves"
}
Mas e a chave type
que nós não passamos em nenhum momento? 🤨
No caso dessa chave, ela é passada de forma automatica pelo próprio Redux, logo assim que chamamos uma de nossas actions
em conjunto com o dispatch
:
O print acima, foi retirado do Visual Studio Code e mostra como isso é feito por de baixo dos panos.
Sendo assim a nossa type
sempre será: user/${string}
, onde:
user/
: é o nome name que atribuímos dentro do reducer
.
${string}
: é o nome da action
em si.
Portanto, o nossa type
seria: user/createUser
🙂
Arquivos da lição
Os arquivos dessa lição podem ser encontrados neste repositório do GitHub.
Conclusão
Nesta lição você aprendeu a criar suas actions com diversos exemplos diferentes para utilizar na sua aplicação 😄
Na próxima lição veremos um conceito novo relacionado ao Redux chamado de Redux Saga (efeitos colaterais)!
Até lá 🥳