Criando Actions dentro dos Reducers

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á 🥳