Description
I asked someone from my team to study the docs about async actions, and he found them very hard to follow. This confirmed my idea that there should be a simpler introduction about this fundamental aspect of using Redux before going into all the details about middleware, how it works etc.
I think a good approach could be to add one last chapter to the "Basics" part of the docs, with a practical explanation about using async. To explain it to my team member I used a simple example, and now decided to write it down in a way that could be used as the base for this new chapter; you can find it below - it's not 100% polished (and not actually tested) because I'm not sure you'll want to use it. If not interested let me know, and I'll recycle it as a blog post :)
Adding async actions
In Redux, basic action creators are pure functions with no side effects. This means that they can't directly execute any async code, as the Flux architecture instead suggests, because async code has side effects by definition.
Fortunately, Redux comes with a powerful middleware system and one specific middleware library, redux-thunk, that allows us to integrate async calls into our action creators in a clean and easy way. This is just a short practical introduction, but you can learn the full story here [link to Advanced chapter].
First of all, we need to integrate the redux-thunk middleware into our store. We can do this in our Todo List example by changing some lines inside index.js; from this:
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './containers/App';
import todoApp from './reducers';
let store = createStore(todoApp);
To this:
import React from 'react';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import App from './containers/App';
import todoApp from './reducers';
let store = applyMiddleware(thunk)(createStore)(todoApp);
Before this change, store.dispatch could only accept a simple Action object. With redux-thunk, dispatch can also accept a thunk function.
- If dispatch receives an Action object, redux-thunk will do nothing, and the reducers will get called with the action as usual, and change the state synchronously.
- If instead dispatch receives a thunk function, redux-thunk will execute it. It will be the thunk function's responsibility to actually dispatch an action object. More importantly, the thunk function doesn't need to be pure, so it can contain async calls, and can dispatch some (synchronous) action only after the async call has finished, also using the data received from the call. Let's see this with a simple example for our Todo app.
In a real app, we'll want to keep our Todo list synchronized with our server. For now we'll only focus on the addTodo action creator; the current one will only update the client side state, but instead we want it to:
- Optimistically update the local state and UI
- Send the data to the server, so it can update the DB
- Manage the server response
To achieve this, we still need our "optimistic" addTodo action creator, but we also need an async thunk action creator to handle the whole async process. To keep the changes to the code at a minimum, we'll rename the optimistic action creator, but not the action type that is sent to the reducers (and used in the switch statement to act). This goes into actions.js:
// renamed optimistic action creator - this won't be called directly
// by the React components anymore, but from our async thunk function
export function addTodoOptimistic(text) {
return { type: ADD_TODO, text };
}
// the async action creator uses the name of the old action creator, so
// it will get called by the existing code when a new todo item should
// be added
export function addTodo(text) {
// we return a thunk function, not an action object!
// the thunk function needs to dispatch some actions to change the
// Store status, so it receives the "dispatch" function as its first parameter
return function(dispatch) {
// here starts the code that actually gets executed when the addTodo action
// creator is dispatched
// first of all, let's do the optimistic UI update - we need to
// dispatch the old synchronous action object, using the renamed
// action creator
dispatch(addTodoOptimistic(text));
// now that the Store has been notified of the new todo item, we
// should also notify our server - we'll use here ES6 fetch function
// to post the data
fetch('/add_todo', {
method: 'post',
body: JSON.stringify({
text
})
}).then(response => {
// you should probably get a real id for your new todo item here,
// and update your store, but we'll leave that to you
}).catch(err => {
// Error: handle it the way you like, undoing the optimistic update,
// showing a "out of sync" message, etc.
});
// what you return here gets returned by the dispatch function that used
// this action creator
return null;
}
}
The above code and its comments show how you can do async calls, mixing them with optimistic updates, while still using the same old dispatch syntax with an action creator. The action creator itself is still a pure function, but the thunk function it returns doesn't need to be, and it can do our async calls. If needed, the thunk can also access the current store state, as it gets getState as its second argument: return function(dispatch, getState) { ...
Redux-thunk is only one example of what middleware can do; to learn more, proceed to the advanced tutorial [link]