Redux hooks code-along: to-do app
A how-to guide for adding this state management library to an existing React app
Yesterday, I made a simple to-do app using React and a Rails backend, which has some functionality of adding and deleting tasks. Though this is small application without large amounts of application state that are needed in many places (in other words, not the best candidate for using a state management library), I thought it would be an easy and clear example of how to add Redux hooks to manage state.
- Note, this tutorial will not go over the backend setup, but you can take a look at that code here
My Current Code
To briefly run through the current structure and behavior of my main component, it is fetching my database at the ‘days’ endpoint (which is in the form of a JSON file) with the hook useEffect
, utilizing useState
to set the result of the fetch to the state (all the days from the database in an array datatype), and rendering the Day component for each day in the state (only dealing with one day for this example).
I do something similar in the Day component — to only see what’s relevant to state management, take a look at these three snippets:
I have fetched all the tasks from my database (with useEffect
again) and set them to a state of allTasks
so that they are all rendered at once, and updated without a page re-load if a task gets deleted. I am passing this state of allTasks
to each Task component, and if I want to delete a task, that function needs to be passed down to be triggered when the button for each Task is clicked.
Let’s start adding Redux into the mix…
Starting with Redux — Explained in words
First, let’s install the redux dependencies (specific for React in my case):
npm install react-redux//or if you are using yarnyarn add react-redux
Let’s define the functions in this app that when called (by some user action or a result of a component mounting) involve changing some data or state —
- fetching the days (GET request)
- fetching the tasks (GET request)
- adding a task (POST request)
- deleting a task (DELETE request)
These will guide our actions, which are:
Actions are not directly responsible for changing the state, however. This is managed by the store, which controls the dispatch, reducer, and current state.
Following the above diagram, our actions are dispatched, and the store updates the state by passing the current state and action to the reducer, and the state gets mutated to what the reducer returned.
Getting into the Code
ACTIONS
So let’s start putting this into code, starting with our actions. To reiterate, actions are a Javascript object that have a type attribute, which describes what happened. For example, instead of using the fetch directly in a useEffect hook, we will place that in a universal action, which will eventually get dispatched in the hook.
Let’s create a new folder from the root directory called actions, and a new file called actionConstants.js
where we define our action types — types should be defined as string constants for consistency’s sake and avoiding errors anywhere in the app.
Let’s make the action for fetching days, in a file called getDays.js
:
getDays is returning a function because we are making use of thunk. More on that later.
We need to import the action types first (I decided to use a success and failure type, but that’s not necessary). Inside the function, let’s make the fetch request return a dispatch of type FETCH_DAYS_SUCCESS
and set the payload (that will be carried along) as the object result of the request, so that when this action is called, it will return this dispatch.
REDUCERS
Reducers are functions that accept state and an action in the argument, which help calculate the new state value, but they cannot alter the existing state. Instead, they must make copies (i.e. with the spread operator, etc.) and make changes to those copies.
Define the state as initialState, and for each action type, have a different return value for the state depending on the action. For getting the days from the database, take a copy of the state and set the days object to the result of the fetch request in the getDays
action (action.payload
), which is an array of day objects.
Thinking of the other reducers we will need, make a rootReducer file, which combines the reducers, ultimately turning different reducing functions into a single reducing function that we can pass to createStore
(which builds the store, holding the complete state tree of the app.)
*TL;DR: reducers are functions that return the next state tree, given the current state tree and an action to handle.
STORE
Now, before we add this to our Main component, we need to wrap our application in a <Provider >… </Provider>
component to render the store available throughout the component tree (so that we can dispatch actions and change state anywhere).
createStore accepts a reducing function and (optional) an enhancer with third-party capabilities such as middleware (the only store enhancer that ships with Redux is applyMiddleware()
. Backing up to that previous mention of thunk in our getDays action creator —
Middleware allows you to write action creators that return a function instead of an action and thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met.
Free code camp provides a great explanation for why it’s useful for this type of application:
One of the main use cases for this middleware is for handling actions that might not be synchronous, for example, using axios to send a GET request. Redux Thunk allows us to dispatch those actions asynchronously and resolve each promise that gets returned.
REDUX HOOKS
Now, moving back to what we need to change in the Main.js file. Instead of using the useState
hook to set our state, we are going to make use of the useSelector
and useDispatch
Redux hooks.
The selector will be called with the entire Redux store state as its only argument, and within that state, we can access the data we need (here, need the days array from the FETCH_DAYS_SUCCESS
action). It will run whenever the component renders. Let’s set it to a days constant so that we can still use the map function to return a Day component for each day.
The useDispatch
hook returns a reference to the dispatch
function from the Redux store, and should be used to dispatch actions. We want to dispatch our action to fetch the days in useEffect
and ultimately have the reducer update the state to that data. Now that we’ve implemented redux for the Main component, the same functionality should be working now as at the beginning of this tutorial.
Why Hooks?
If you’re familiar with Redux , you’ll notice we haven’t used the higher order function connect()
, mapStateToProps
, or mapDispatchToProps
. These hooks give access to the Redux store and dispatch actions without having to wrap your components in connect()
, allowing for less and more semantic code in my opinion.
OTHER ACTIONS & REDUCERS
Virtually the same logic is employed for fetching and displaying the tasks in the Day component, so I will not rehash that process, and adding / deleting tasks is only slightly different.
Starting with the addTask
action creator, the only main difference from the getDays
creator is the necessary arguments required for a successful POST request (since we need to define the attributes necessary to create a new task).
Additionally, we set the payload
to the new task created.
Let’s take a look at the tasksReducer
next —
The tasksReducer
contains a switch statement for each action that can be dispatched. Here, for the add task action, we are pushing the new task (action.payload
) into the task array, and returning that updated state. You can apply similar logic for deleting a task (action creator not shown here) for returning a state with the deleted task removed.
These two snippets (above and below) of the Day component show how we are using useSelector
and useDispatch
to access the store, make use of the state, and dispatch actions. The add
function dispatches addTask
, accepting the arguments needed to pass to the action creator for the POST request.
Here, the component is mapping over the allTasks array (which we got from the useSelector
hook) to render a Task component for each object in that array.
If you made it this far, hopefully you found this helpful as an introduction to Redux hooks (or Redux overall) as a state management option available for your next React app. To see the entirety of my code, you can take a look at my github repo here.