Simple Asynchronous Requests in React using Redux-Thunk
Why Redux?
As React projects begin to grow, the components dependency upon the application’s state grows with it. Eventually the passing of state and props between components can become a complex web that can become hard to understand. This not only creates more room for component misbehavior, but also muddies a developers ability to understand how components are handling and sharing data with each other.
Redux solves this issue by providing us a predictable container in which to store our state outside of our components. The benefit of this approach is it allows our components to no longer be concerned with logic that updates state. Instead components can be simplified to dispatch events and display the new state that is the result of those events.
Similar to component state, in Redux all of our data is held in an object. The difference with Redux, being a single source of state outside of our component tree, is that any component can grab any part of this data that it needs just by connecting to the Redux store.
Our data is now all in one place. Our state is just a plain JavaScript object. Nothing fancy, but the data flow pattern that Redux provides paves the way for cleaner, more comprehensible code.
That said, Redux isn’t one size fits all. It can be helpful when planning your application design to ask yourself “why Redux”? Implementing Redux on smaller size applications might be overkill, and you’ll find even when using it on larger scale projects there are times when a component can more easily use it’s local state to track changes without needing to pass that information to any other component.
Basics of Redux
As this article is intended as a look into implementing asynchronous actions with Redux, I will only give a brief overview of the core Redux concepts here. Do check out the Redux documentation for a deeper dive.
There are three main aspects that form the basis of Redux: actions, reducers, and store. The flow of data between these three aspects can be simplified into three phases:
- An action is called that is then
- Sent to a function that then
- Updates our state according to that action
Action -> Function -> Updated State
An action is a plain JavaScript object with a type attribute. This type attribute describes what you would like to do, for example UPDATE_COMMENT
or FETCH_DATA
.
In step two we implement the logic of these actions in a function called a Reducer. A reducer is a function that takes in your action, your previous state, and depending on that action, updates your state. Reducers are written as switch statements, a conditional that updates and returns a new state according to which action is passed in.
Now that we have introduced reducers we can give a name to our “function” in our data a flow above and rewrite it as:
Action -> Reducer -> Updated State
No matter how complex a Redux implementation becomes, it always follows this pattern of flow.
But now that we’ve updated our state, how do we access this data?
That is where store comes in. Store is our home for the state of our entire application. If that sounds daunting do remember that state is just a plain JavaScript object with key/value pairs like any other object. It actually makes grabbing the slices of state that a particular component needs at any given time a piece of cake.
A Note on React Hooks
With it’s version 16.8 release React introduced hooks, a simple way of using state and other React features without needing to create a class component.
React-Redux, the UI binding library that connects Redux to React has added two hooks of it’s own so that grants our components easy access to our Redux store: useSelector and useDispatch.
Getting Started
First we need to create a new store in a store.js file like we did above. We import createStore
from the Redux library and pass it our reducers. This store instance will then be what we pass in to our application.
We import Provider in our index.js file and use it to wrap our application. We pass this Provider our store we just created which will now give the ability to connect to the store to all components in our application.
The Conundrum of Asynchronicity
For this example we are going to be making a fetch request for a cryptocurrency React app that fetches and displays market data for the 100 most popular cryptocurrencies. We will be using the free crypto data API over at CoinGecko.com to make our requests.
Our implementation of state here will be simple, but let’s try to imagine it within the context of a larger scale application. We will only be setting and getting state for our coin data, but lets pretend it is happening in an app that is also storing user data, wallet data, graph data and so forth. A use case such as this would be a proper justification for using Redux to manage state whereas an app that just uses state in the manner we are about to show may not benefit from using Redux.
We will start by creating our action in our reducers switch statement “SET_COINS”
(Note: the convention for the cases in our switch statement are written as “DO_ACTION”. I like to set all of these to constants and store them in their own file constants.js. The example we are using would be written as export const SET_COINS = "SET_COINS"
. This is optional, but it protect against bugs that could be introduced via typos)
Next we will create a functional component called CoinContainer
and import useSelector
and useDispatch
from react-redux
. We will call useSelector
to grab our coins from our state, and we will set dispatch to useDispatch
to use for our action calls.
Great, now we just need to fetch us some coins!
How might we go about making a fetch request with this current setup?
The first configuration that might come to mind is using React’s useEffect hook with an empty dependency. Calling this hook in this manner is similar to using the lifecycle method “component did mount” in that it waits until the component has mounted before making this request. For more information on useEffect check this out. Lets set up a fetch request here and see what happens.
Notice how we are importing and sending a function to our dispatch called fetchCoins
. We are going to create this function in it’s own file called coinActions.js
. This is what in Redux is called an action creator, a function that returns an action object for our dispatch.
Everything looks good. We are setting up our fetch request to our API URL and then dispatching the data returned back to the SET_COINS action in our reducer. We should expect to be able to display our coin data now, right?
Not quite. Fetch requests return something called a Promise. A Promise object is an object that represents some value that will be available later. We can access this value when it “resolves” by chaining then() functions on to our fetch function. We can then take this response and parse it into JSON.
Even so, our fetch function is still going to return before our Promise is resolved and our code will continue moving down the stack, meaning our return is going to send our object to our dispatch before it has any data to dispatch.
Something else to consider is that we always Redux to be reflecting the current applications state so it would be optimal to reflect the state in between a request for data and actually receiving the data. It follows then that we would 1.) like a function to dispatch an action that sets our state to loading when a request is made, 2.) makes the actual fetch request, and then 3.) once that request resolves dispatches another dispatch to add the data to our state (and while we are at it 4.) a dispatch to catch any errors that may occur from our request would be helpful too).
If only there was a way!
Thunk Middleware to the Rescue
Redux runs synchronously, triggering one action after another. To make asynchronous requests we will utilize a middleware library called Redux-Thunk. Thunk is going to check if our action is returning a function or an object. As we remember from earlier, normally an action is just a plain JavaScript object. Now every time we trigger an action it will be checked by redux-thunk to see if it returns an object or a function and if it is returning a function it will pass a dispatch argument to your function which you can then use to dispatch actions at the appropriate time.
To utilize Thunk we need to first install redux-thunk
and make some adjustments to our imports.
We will import in applyMiddleware
pass it in as our second argument to our createStore
. finally we will pass applyMiddleware(thunk)
that we have imported from redux-thunk
.
Let’s see how our fetchCoins function might be rewritten using the logic of Thunk. Remember all we have to do on our end is return a function instead of an object and expect that function to be passed dispatch as an argument. We chain these calls together and Thunk will take care of executing the code at the proper time for us.
Nice! We can see that by using this pattern we meet all the requirements of a
proper fetch action that we laid out above:
- we dispatch an action to set our state to loading
- we make the fetch request
- we dispatch an action to update our state once our request resolves
- we have a third dispatch to catch any errors should our request fail
Now we should be connected to our Thunk-ized action creator from our useEffect
call we made early, from the Thunk-ized function to our reducer and from our reducer to our store and back up to our component!
We can see our data is being added to state and now we can use our state to iterate over our data and display the data returned in any way we wish, perhaps even combining this request with other actions to set and update our state elegantly throughout a full application.
Conclusion
Redux may not be the best option in every application configuration. But when your apps do begin to grow and complexify Redux provides a reliable way to store your state in one place, freeing up your components to simply focus on what to do with the data they receive instead of worry about storing state and passing it along. The addition of React Thunk builds upon this data flow by allowing asynchronous requests to flow into this pattern seamlessly with only a few lines of additional code.
Thank you for checking out this tutorial! I welcome your feedback.
Happy coding!