One of the most important tasks of creating front-end applications is fetching data and showing it to users. With React and Redux, we usually need to write a lot of boilerplate code to handle those requests. When multiple components need to access data from the same REST API, each component needs to check if data exists, and if not, then data needs to be fetched separately. To simplify this general process, we introduced a new pattern to provide a unified caching mechanism, error handling and loading status management. The pattern is widely used in building eBay internal cloud platform portals and it increases the development efficiency by simplifying code of components.
We call the pattern Auto Effect. It makes Redux async actions of fetching data easier and re-useful in a web application built with Redux and React hooks. A Redux async action, either an existing one or a new one, can use the pattern to wrap it as a React hook to make it easier to use.
Note this pattern is only useful for the scenario:
- Use React functional components
- Use Redux async action to handle HTTP requests
- Keep data, error and loading state in the Redux store
Redux Async Action Development Process
Whenever a component needs to show some data from remote API, we usually need to implement the below things:
- Define a Redux action to fetch data, dispatch fetchDataBegin, fetchDataSuccess, and fetchDataFailure actions when request status changes.
- Handle those actions in the reducer, set fetched data, pending, error state in the Redux store.
- Connect the component to the store and render the UI based on the current state, like show loading, error or requested data.
With this process, we create `action.js`, `reducer.js` and `Component.js` files. These are normal Redux practices, if you are not familiar with it you can see the async action sample from Redux's official documentation.
First, let's see how we use the async action in a component without the Auto Effect pattern.
The Existing Approach
Say that we've created an async action to fetch data. For example, it's named "fetchData", then we can use it in a React component:
The most painful part of this approach is every component that needs the data has to implement the same logic when using the action:
- Use useEffect to call the action
- Implement cache logic, if data exists, then use the existing one
- Know the exact path of the data in the store so that it can use “useSelector” to bind components to the store
We can move the logic in a hook so that it could be re-useful for all components. Then we can think of the REST API as a remote data source.
From component perspective it only needs to care about three parts of any REST API:
- Data: the data returned from API when successfully.
- Pending: whether it's fetching the data.
- Error: if the request failed, we know it’s from the error state.
Next let's see how to create a React hook for the async action.
Auto Effect Pattern
To simplify the usage of async action, we can create a hook named "useFetchData" to wrap the Redux action as a hook:
In this hook, we guarantee the existence of the data. That is if it doesn't exist then fetch it, otherwise it does nothing. With this hook, we can treat any REST API as a data source which contains status if data is available:
Then we can use the remote data via a React hook in any component:
Then it is very easy to be used in a component with much less code. We no longer care about:
- Whether data exists.
- How to call API to fetch data.
- Where is the state saved in Redux store and how to connect it to the component?
That's just why we call it Auto Effect Pattern. It uses useEffect to call API automatically to guarantee the data exists when the hook is called.
By this approach, any component that needs the remote data doesn't need to implement any logic to call action to fetch data. The common logic is abstracted into the hook so that the action is more re-useful.
To be simple, the above example async action has no arguments. However, in real cases some async actions receive arguments to pass to the REST API. For example, if a component shows an article by the article id passed in as a property. Then we just need to slightly modify the hook:
By this approach, whenever article id is changed, the hook guarantees the API is called to fetch the new article.
Manual Auto Effect
In the example, we can see the API is always called when the component is mounted. However, that doesn't always make sense, in some components, the data is only fetched when a button is clicked. So, we should also allow the hook to be called without auto effect enabled. To make this happen we can do some tricks to the arguments of the hook:
Then if the hook is called without any argument then it doesn't call the action in the useEffect logic. If we look back to the hook implementation, we have also exported bound action in the hook, so we can use fetchData directly without dispatching the return value.
Auto effect is only triggered if any argument is passed to the hook. Even if the action has no argument, we also need to pass it an empty array to trigger the auto effect logic. If there're arguments for the action, we need to embed all arguments in an array to be passed to the hook.
By this trick, we can wrap actions with the auto effect hook safely. It doesn't bring any limitation to the existing code logic. We can either use the Redux in the old way or use the hook with auto effect.
Wrap All Redux Actions Into Hooks
As mentioned, the auto effect pattern only seems to be useful for HTTP requests of get method. However, in the hook there are other logic besides auto side effect:
- Bind Redux action with dispatch
- Use “useSelector” hook to bind the component to the store
In our practice, almost all Redux actions are wrapped in hooks even if there's no auto effect logic necessary, including both sync and async actions. It also makes actions easier to reuse because some common logic is implemented in the hook. Take a simple counter action as example, we wrapped it in a hook:
The approach is useful because almost everywhere we use a Redux action we also need to access some specific value of the Redux store. So, it makes sense to encapsulate the logic of action binding and store binding in hooks.
Why Not Provide it as a Library?
It looks like that the pattern could be provided as a library via npm. But we think the hook logic should be very flexible to meet different requirements rather than encapsulate everything into a module, like:
- Custom cache logic. For example, use reselect to improve performance
- More return value from the hook. In our scenario, we also return “dismissFetchDataError” to clear error state from the hook. So, you can customize the hook by your requirement.
In this article, we introduced a new pattern for managing REST API requests with Redux actions and React hooks. The pattern is widely used for building eBay internal cloud platform portal and we created code generators to reduce the boilerplate coding effect. It separates concerns of HTTP requests from components into hooks. By thinking of the API as a remote resource, it makes Redux actions easier to use.