When writing React applications with Redux you’ll no doubt need to call upon a web service of some kind to hydrate your components with information.
But where should those api calls go?
Inside Container Components
The most obvious place would be the components themselves, placing the api calls in container components high up in your component hierarchy to pass down to presentation components as props:
class UserPage extends Component {
componentWillMount(){
this.updateUser(this.props.userId)
}
componentWillReceiveProps(newProps){
if(newProps.userId !== this.props.userId){
this.updateUser(newProps.userId)
}
}
updateUser(userId) {
request.get('/api/user/' + userId)
.end( function(err, res) {
this.setState({user: res.body})
})
}
render(){
return (
<UserInfo user={this.state.user} />
)
}
...
}
However, this means that container components need to manage state: the response from the api calls, and any errors that occur.
You probably also want to make api calls asynchronous where you can. The example above will block on componentWillMount()
, delaying the initial render of the component, so we would want to wrap the call in a promise or generator. We also probably want to store state of when we’re in the middle of a request so components can put up a spinner. Which is at least 3 different ‘modes’ for the container to be in: fetching, received a successful response, or received an error. Having more than one API call in a component multiples this by at least another 3. These components become harder to reason about and tend to be bug prone.
On top of that, unit testing the components becomes more difficult, as it then necessary to mock the api calls. Compare this with redux, where part of the store are received via props. There is no abstraction between what data the component requires, and how that data is requested. We need to separate our concerns.
Inside Reducers
We want to avoid state in components where we can, and using Redux is a great way to accomplish this. If we could make our API calls from within redux, components can simply subscribe to API updates from the Redux store. No need for big, stateful components that are hard to reason about.
But this doesn’t feel quite right: reducers are only supposed to act on the contents of actions, not any heavy lifting. Not only that - reducers are synchronous. We don’t want an API call to block all state changes to the entire application!
Let’s recall Async Actions (redux docs):
When you call an asynchronous API, there are two crucial moments in time: the moment you start the call, and the moment when you receive an answer (or a timeout).
Each of these two moments can usually require a change in the application state; to do that, you need to dispatch normal actions that will be processed by reducers synchronously. Usually, for any API request you’ll want to dispatch at least three different kinds of actions:
An action informing the reducers that the request began.
An action informing the reducers that the request finished successfully.
An action informing the reducers that the request failed.
So we want to dispatch an action from our container component, which dispatches another action when the request is sent. Then When the response from the server comes back, dispatch a success action or error action, depending on whether the request was successful.
We need to repeat this pattern for every single API endpoint…
Middleware to the Rescue
Redux middleware lets us intercept redux actions before they pass through the redux pipeline to the reducers, which makes them perfect for handling cross-cutting concerns.
The excellent redux-api-middleware library lets us declaratively specify the actions, along with the three aforementioned API actions for the middleware to dispatch on our behalf.
import { CALL_API } from 'redux-api-middleware'
export const USER_REQUEST = 'user/REQUEST'
export const USER_SET = 'user/SET'
export const USER_ERROR = 'user/ERROR'
export function updateUser(userId) {
return {
[CALL_API]: {
endpoint: '/api/user/' + userId,
method: 'POST',
types: [USER_REQUEST, USER_SET, USER_ERROR]
}
}
}
Then our reducer can then update the store as actions are emitted from the api middleware:
import { handleActions } from 'redux-actions'
const user = handleActions({
[USER_REQUEST]: (state, action) => ({
isFetching: true,
isError: false,
user: state.user
}),
[USER_SET]: (state, action) => ({
isFetching: false,
isError: false,
user: action.payload // the API response body
}),
[USER_ERROR]: (state, action) => ({
isFetching: false,
isError: true,
user: null
})
})
export default user
Summary
The ecosystem is evolving at a furious pace: with no shortage of other solutions: ES6 Generators, the new Fetch API, and libraries such as react-refetch.
But if you’re wondering where to put your api requests in your Redux-enabled application, middleware is the way to go.
Credit: Photo by Daniel Hansen on Unsplash
Top comments (0)