One of the things almost every React app will do at some point is fetching data from an API. While most tutorials will be quick to state a single way of doing that, at Zweitag we were looking for something with less boilerplate, that is easy to extend and maintainable.
The Obvious Way: Fetch it where you need it
The solution you'll most commonly see is to write requests directly into the component. A get-request can easily be done from the componentDidMount method.
This is a fairly straightforward way, and we'll be able to use the data immediately inside our component through this.state.user. But what if we need the same data in a different part of our app? We might end up fetching the same data in multiple components, even if it should already be available.
The DRY Way: Fetch it once, use it everywhere
Redux is not only great for managing your app state, it might also prevent sending unnecessary requests by providing the fetched data to all components that need it.
The typical way of fetching data in Redux apps is the creation of a Thunk. It allows us to dispatch actions after we've done something else, e.g. sending a request.
A reducer can catch this action and store the user data for all components. We still can dispatch the fetchUser action from componentDidMount, but most of the time it is better to fetch the data per route. At Zweitag we like to use Redux First Router, with its onBeforeChange option to define the data needed for each route.
The Zweitag Way: Do it Smart
While dispatching a thunk might be a good fit for apps which only need to fetch data every now and then, it doesn't scale very well. This becomes obvious as soon as we want to implement error handling or a loading state. With the solution above we'd have to wire up every action with a try/catch statement and additional dispatches for changing the loading state.
Wouldn't it be great if we only had to dispatch actions like this:
This action doesn't know any details about the actual request, errors or loading state. It doesn't even describe what happens with the fetched data. Everything of importance can happen in its own module, a Redux middleware.
There are some request middlewares out there which might work for this, but at Zweitag we felt it would be better to keep it small and adjust it to specific needs of each app. The foundation always looks like this:
A successful action gets a responseData attached and will be forwarded to the reducers. There you could check for responsibilities:
Fetching more data for our app will be as simple as adding a new (small) action, and setting up a reducer to store the response data.
With that barebones request middleware we can go ahead and enhance it with all kinds of functionality.
- Error handling can happen through another middleware which is chained after the request. It only has to listen for action.payload.error and react accordingly.
- To handle the loading state another middleware can listen to SEND_REQUEST coming through and dispatch SET_LOADING actions.
- Let's say you want to react to a request not only in the reducer, but by dispatching another action. We can add onSuccess methods to the request action and dispatch them from the middleware.
- Even more, you can add onError and onFinally for maximum flexibility.
At Zweitag we often use a different combination of all these features, so that we have a custom middleware with all the things we need without bloating the code. That way we can improve on this functionality while developing exactly what the app needs.