Introdução
Por padrão, as ações do Redux são enviadas de forma síncrona, o que é um problema para todos os aplicativos não triviais que precisam se comunicar com uma API externa ou executar efeitos colaterais. O Redux também permite que middleware fique entre uma ação sendo despachada e a ação que atinge os redutores.
Existem duas bibliotecas de middleware muito populares que permitem efeitos colaterais e ações assíncronas: Redux Thunk e Redux Saga. Neste post, você irá explorar o Redux Thunk.
Thunk (conversão) é um conceito de programação onde uma função é usada para atrasar a avaliação/cálculo de uma operação.
O Redux Thunk é um middleware que permite chamar criadores de ação que retornam uma função em vez de um objeto de ação. Essa função recebe o método de expedição do armazenamento, que é usado então para expedir ações síncronas regulares dentro do corpo da função assim que as operações assíncronas forem concluídas.
Neste artigo, você irá aprender como adicionar o Redux Thunk e como ele pode se encaixar em um aplicativo Todo hipotético.
Pré-requisitos
Este post assume que você tenha conhecimento básico do React e do Redux. Confira este post se estiver iniciando com o Redux.
Este tutorial é construído a partir de um aplicativo Todo hipotético que rastreia tarefas que precisam ser realizadas e foram concluídas. Assume-se que o create-react-app
foi usado para gerar um novo aplicativo React, e o redux
, react-redux
e axios
já foram instalados.
Os detalhes mais finos sobre como criar um aplicativo Todo do zero não serão explicados aqui. Ele será apresentado como um cenário conceitual para evidenciar o Redux Thunk.
Adicionando o redux-thunk
Primeiro, use o terminal para navegar até o diretório do projeto e instale o pacote redux-thunk
em seu projeto:
- npm install redux-thunk@2.3.0
Nota: o Redux Thunk possui apenas 14 linhas de código. Confira aqui o código fonte para aprender sobre como um middleware Redux funciona nos bastidores.
Agora, aplique o middleware ao criar o armazenamento do seu aplicativo usando o applyMiddleware
do Redux. Em um dado aplicativo React com redux
e react-redux
, seu arquivo index.js
deve ficar assim:
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import './index.css';
import rootReducer from './reducers';
import App from './App';
import * as serviceWorker from './serviceWorker';
// use applyMiddleware to add the thunk middleware to the store
const store = createStore(rootReducer, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Agora, o Redux Thunk é importado e aplicado em seu aplicativo.
Usando o Redux Thunk em um aplicativo de amostra
O caso de uso mais comum para o Redux Thunk é para se comunicar de forma assíncrona com uma API externa para recuperar ou salvar dados. O Redux Thunk torna mais fácil expedir ações que seguem o ciclo de vida de uma solicitação para uma API externa.
Criar um novo item de tarefa pendente normalmente envolve primeiro expedir uma ação para indicar que a criação de um item de tarefa pendente foi iniciado. Em seguida, se o item de tarefa for criado com sucesso e retornado pelo servidor externo, expedindo outra ação com o novo item de tarefa. Caso aconteça um erro e a tarefa não seja salva no servidor, uma ação com o erro pode ser expedida em vez disso.
Vamos ver como isso seria feito usando o Redux Thunk.
Em seu componente contêiner, importe a ação e emita-a:
src/containers/AddTodo.js
import { connect } from 'react-redux';
import { addTodo } from '../actions';
import NewTodo from '../components/NewTodo';
const mapDispatchToProps = dispatch => {
return {
onAddTodo: todo => {
dispatch(addTodo(todo));
}
};
};
export default connect(
null,
mapDispatchToProps
)(NewTodo);
A ação irá usar o Axios para enviar uma solicitação POST
ao ponto de extremidade em JSONPlaceholder (https://jsonplaceholder.typicode.com/todos
):
src/actions/index.js
import {
ADD_TODO_SUCCESS,
ADD_TODO_FAILURE,
ADD_TODO_STARTED,
DELETE_TODO
} from './types';
import axios from 'axios';
export const addTodo = ({ title, userId }) => {
return dispatch => {
dispatch(addTodoStarted());
axios
.post(`https://jsonplaceholder.typicode.com/todos`, {
title,
userId,
completed: false
})
.then(res => {
dispatch(addTodoSuccess(res.data));
})
.catch(err => {
dispatch(addTodoFailure(err.message));
});
};
};
const addTodoSuccess = todo => ({
type: ADD_TODO_SUCCESS,
payload: {
...todo
}
});
const addTodoStarted = () => ({
type: ADD_TODO_STARTED
});
const addTodoFailure = error => ({
type: ADD_TODO_FAILURE,
payload: {
error
}
});
Observe como o criador de ação addTodo
retorna uma função em vez do objeto de ação regular. Essa função recebe o método de expedição do armazenamento.
Dentro do corpo da função, envia-se primeiro uma ação síncrona imediata para o armazenamento para indicar que iniciou-se o salvamento da tarefa pendente com a API externa. Em seguida, você faz a solicitação POST
real ao servidor usando o Axios. No caso de uma resposta bem-sucedida do servidor, você expede uma ação de sucesso síncrona com os dados recebidos da resposta, mas para uma resposta de falha, envia-se uma ação síncrona diferente com a mensagem de erro.
Ao usar uma API externa, como o JSONPlaceholder neste caso, é possível ver o atraso de rede real acontecendo. No entanto, se estiver trabalhando com um servidor de backend local, as respostas de rede podem acontecer muito rapidamente para visualizar o atraso de rede que um usuário real estaria observando. Sendo assim, é possível adicionar um atraso artificial ao desenvolver:
src/actions/index.js
// ...
export const addTodo = ({ title, userId }) => {
return dispatch => {
dispatch(addTodoStarted());
axios
.post(ENDPOINT, {
title,
userId,
completed: false
})
.then(res => {
setTimeout(() => {
dispatch(addTodoSuccess(res.data));
}, 2500);
})
.catch(err => {
dispatch(addTodoFailure(err.message));
});
};
};
// ...
Para testar cenários de erro, emita manualmente um erro:
src/actions/index.js
// ...
export const addTodo = ({ title, userId }) => {
return dispatch => {
dispatch(addTodoStarted());
axios
.post(ENDPOINT, {
title,
userId,
completed: false
})
.then(res => {
throw new Error('addToDo error!');
// dispatch(addTodoSuccess(res.data));
})
.catch(err => {
dispatch(addTodoFailure(err.message));
});
};
};
// ...
Para fins didáticos, aqui está um exemplo de como o redutor de tarefa pendente poderia ser para lidar com o ciclo de vida completo da solicitação:
src/reducers/todosReducer.js
import {
ADD_TODO_SUCCESS,
ADD_TODO_FAILURE,
ADD_TODO_STARTED,
DELETE_TODO
} from '../actions/types';
const initialState = {
loading: false,
todos: [],
error: null
};
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO_STARTED:
return {
...state,
loading: true
};
case ADD_TODO_SUCCESS:
return {
...state,
loading: false,
error: null,
todos: [...state.todos, action.payload]
};
case ADD_TODO_FAILURE:
return {
...state,
loading: false,
error: action.payload.error
};
default:
return state;
}
}
Explorando o getState
Além de receber o método de expedição do estado, a função retornada por um criador de ação assíncrona com o Redux Thunk também recebe o método getState
do armazenamento, de forma que os valores atuais do armazenamento possam ser lidos:
src/actions/index.js
export const addTodo = ({ title, userId }) => {
return (dispatch, getState) => {
dispatch(addTodoStarted());
console.log('current state:', getState());
// ...
};
};
Com o código acima, o estado atual será impresso no console.
Por exemplo:
{loading: true, todos: Array(1), error: null}
Usar o getState
pode ser útil para lidar com as coisas de maneira diferente dependendo do estado atual. Por exemplo, se quiser limitar o aplicativo a apenas quatro itens de tarefa por vez, você pode retornar da função se o estado já possuir a quantidade máxima de itens de tarefa:
src/actions/index.js
export const addTodo = ({ title, userId }) => {
return (dispatch, getState) => {
const { todos } = getState();
if (todos.length > 4) return;
dispatch(addTodoStarted());
// ...
};
};
Com o código acima, o aplicativo ficará limitado a quatro itens de tarefa.
Conclusão
Neste tutorial, você explorou adicionar o Redux Thunk a um aplicativo React para permitir a expedição de ações de maneira assíncrona. Isso é útil ao usar um armazenamento Redux e APIs externas.
Se quiser aprender mais sobre o React, dê uma olhada em nossa série Como programar no React.js, ou confira nossa página do tópico React para exercícios e projetos de programação.