Reasoning behind using Redux Toolkit’s createSlice and createAsyncThunk to handle your asynchronous state changes
I am a great proponent of having sound reasoning behind development decisions. It may sound odd when stated explicitly, but a lot of the time we do stuff in web development because it’s a) already being done that way on the project, b) currently the popular way of doing things. What I want to cover in this article are the steps listed below:
- recognize a situation where Redux is applicable
- provide step by step implementation
- back up the implementation with said sound reasoning as we go along
For quick reference, I will be using snippets from these two repositories as my before and after shots.
Chapter I: The setup
Let’s say I want to show a list of data. As all data, it’s fetched via an asynchronous API call. First thing’s first — I like to build components like this in two quick breaths (or often times more).
I want a “pure” component (not to be mistaken with React.PureComponent) that only displays passed in props as JSX. The purity I’m talking about here is conceptually similar to “pure functions”. A pure function is a function that is fully deterministic, meaning it has no side effects. To put it plainly, it always renders the exact same output for the exact same input. These “pure” components have no logic, apart from conditional rendering, but even here I would thread carefully. This gives me an easily presentable, purely UI component, which, for example, doesn’t require a lot of effort to get rigged in a storybook story and shared with the rest of the team. For reference, this is a good example of what I’m talking about:
So far so good.
The “meat” of the component is inside the parent. The parent component handles fetching data, displaying a loading indicator and possibly catching an error. To achieve functionality on component mount for functional components, we use the useEffect
hook with an empty dependency array. We gather variables that need to persist after re-render with our useState
hook and we conditionally render a loading and/or an error message. This will look roughly as follows (I’ve omitted the error handling to keep the example simpler):
A couple of things are OK in this approach, and a couple of things would make the hair on the back of my neck stand up if I was asked to review this in a PR on an enterprise level project.
The good stuff. We’ve made peace with the linter and our async function is inside the useEffect
hook whilst keeping our callback function nominally synchronous and the dependency array empty. I always like seeing an empty dependency array as it is clearer that this effect is supposed to happen on component mount and component mount only. Also, we’re wrapping our unpredictable API call inside a try catch block, so any unexpected behavior is inside a small local error boundary.
The bad stuff. For me personally, a long winded useEffect
is the hallmark of beginner React programmer. For one, you’re very bluntly relying on a side effect to properly render your component. Another thing is, you’re making your colleagues read through a block of code which is poorly encapsulated — perhaps they’re not here because of anything connected to your block of code, but they’ve just wasted precious time interpreting something which could be literally anything at first glance. To top it all off — and this is the main reason I WOULD NOT ever handle this in this particular way — in order to cover this behavior in a unit test tied strictly to this component becomes impossible. You have to mock the API call and even then you’ll have to bring in test id’s for your html and verify it’s existence (or non existence) inside the document. Oh, and you’re also rendering and re-rendering a component mid test and hoping the useEffect
hooks works. Firstly — of course it works, you shouldn’t test 3rd party libraries, and secondly, you’ve just created a black box test inside a unit test. Congratulations, your unit test is an integration test which probably doubles up the effort, as this should be covered in an end to end test.
Chapter II: The reckoning
As we’re rightfully unhappy with the “simple” approach, how would we refactor this code? Bare in mind, the solution I am about to propose is in line with a project set to last, spanning over months and months of work, several teams working together with programmers of all shapes and sizes.
With the advent of React hooks, for me, Redux has become a much more workable solution in terms of the aesthetic impact of our code. I won’t go into great detail about how redux works, but rather jump into implementing it straight away. After running
yarn add redux react-redux @reduxjs/toolkit
we should be on our way.
The first thing you need to do is to wrap a redux Provider
component around the area you wish to operate on. It’s safe to wrap it at the top level as redux is quite performant and won’t trigger unnecessary rerenders upon global store changes (like what might happen if you were to use React Context API).
The store object is provided by us. A store created using some of the benefits of the redux-toolkit
looks roughly like this.
Nothing fancy, we use the configureStore
function to create a store out of reducers we have written. To keep top level reducers manageable and operable by several teams at the same time, best practice is to split them into slices. Below is a very basic slice created with another toolkit utility function called createSlice
.
Do read up on everything createSlice
offers in the official documentation. What I’d like to mention here is that, although it may look like you’re breaking the flux way of doing things and changing the immutable state directly, what you’re actually doing is just using immer
under the hood and your state in fact stays immutable. The createSlice
automatically creates actions you can call anywhere from your code and a reducer for that particular module — an easily manageable “slice” of state and reducer logic, you can use to encapsulate logic per business concern.
But, we’re not done yet. We still want to keep our components cleaner, and move async logic into our asynchronous thunks
. We could use the above logic and call it inside our component at appropriate times, but with just that, we haven’t fully abstracted the implementation away from our reusable components.
What we actually want to do is use createAsyncThunk
, and, while it has many options of it’s own which are well worth exploring, a very simple implementation would look something like the code presented below:
So now we have the entire logic necessary for an asynchronous data fetch inside it’s own function using (hopefully) already tested and trustworthy reducer logic. Speaking of tests, I will cover those in a separate article, but for now we’ve scratched the surface of what createAsyncThunk
and createSlice
offer, and what was our main goal all along. We have logic that is:
- encapsulated
- testable
- trustworthy
- well documented
Using this in our component will look like this:
Scroll up and compare this to what we’ve started with. We’re actually achieving more with less freehand code polluting our components. The useSelector
hook makes sure our component state stays up to date, and all we have to do inside our component is dispatch our thunk.
Chapter III: The gist of it
What I like about this approach is that, this last step can be done by someone completely unfamiliar with the entire redux flow and slice logic is fully encapsulated so different developers and different teams can stay out of each other’s way, all the while developing globally available functionality.
An added bonus we get is that well tested reducer actions get used by encapsulated and easily testable chunks of logic (thunks), which are in turn called inside our component by reliable methods.
What I’m saying is, these thunks are often dispatched onClick
or on component mount, meaning that we barely have to spend time unit testing them inside the end product (component), as they are a) already tested on their own, and b) called by either a useEffect
hook or a user’s interaction with the browser — these are both things we can fully trust as the teams inside Facebook (React) and Google (Chrome) probably did their due diligence before publishing their software.
Conclusion
I hope I’ve given you enough food for thought and convinced you to explore the redux toolkit. It really is an amazing package and does a great job at supplementing a redux implementation into your React app. I’ve barely scratched the surface of what createSlice
and createAsyncThunk
are capable of and I still owe you an explanation on how to exactly unit test them, but I’ll leave that for another time. Also, I’ve left out a really powerful approach where we leverage promise life cycle actions provided by createAsyncThunk
, but again, I hope to cover that as a more advanced use case in the future.
If you liked the text or just want to grow your network, feel free to connect on either twitter or linkedin.