Headless WooCommerce & Next.js: Set up Redux Toolkit for State Management
At some point we have to deal with state management. I think the useState
hook is great for most situations but the ever-popular Redux gives you that bit more flexibility across different components. In this article I’ll show you how I use Redux Toolkit to manage state for a cart.
I have learned to like Redux but it was a little tricky to get my head around at first. Redux store, reducers, actions, types, slices, dispatch, selectors… There’s a fair bit of terminology to keep in mind but Redux is particularly useful for larger projects which is primarily why I persevered.
React Context
API is another way of sharing data across components without having to pass props down manually at every level.
On Redux’s official website, they tell you that, “Redux Toolkit is our official recommended approach for writing Redux logic”. I figured it’s probably best to follow their recommendation and their Redux Toolkit website contains good documentation and tutorials to follow.
Installing Redux Toolkit
yarn add @reduxjs/toolkit react-reduxnpm install @reduxjs/toolkit react-redux
As of React Redux v7.2.3, the react-redux
package has a dependency on @types/react-redux
, so the type definitions will be automatically installed with the library. Otherwise, you'll need to manually install them yourself (typically npm install @types/react-redux
or yarn add @types/react-redux --dev
).
Configure the Store
At its core we need to set up a Redux store so we can save and retrieve data from the store within any functional component. To do this, I like to create a \store
folder in the root and keep everything in one place.
You can see we have created a store using configureStore
and here we can pass multiple reducers into the store to keep things organised. Think of these reducers as state containers. Each container is essentially an object of key-value pairs storing data in a structure you define. I included a cartReducer
which we will come to in a moment.
Create Typed Hooks
Redux has useDispatch
and useSelector
hooks that help us update the state and retrieve the state, respectively. With TypeScript apps Redux Toolkit suggests creating typed versions of the useDispatch
and useSelector
hooks to make it easier and more reliable to use throughout your app.
With the RootState
type and AppDispatch
type exported in the store.ts
file we can create a separate hooks.ts
file and create the typed versions of useDispatch
and useSelector
. These can then be imported into any component file that needs to use the hooks.
Create Redux Slices
The way I like to think of slices is that a Redux slice allows us to manage state containers. In the example store.ts
above I passed in cart: cartReducer
as one of my state containers. For each state container we will want to define what data it stores as its state and also define specific actions that let us change the state (e.g. one action to increase a number counter by 1 or another action to toggle a boolean flag).
The example above gives an idea of the structure for a slice but it still needs more work to define the reducers and export them as actions.
The first thing we’re doing is defining the interface type for our state data — I’m calling it CartState
. For our CartState
all I want to store is an array of LineItem
for now. You can add more data and define it as necessary.
Now that we know the interface has been defined we can move on to define what we want our initial state to be. I want to start with an empty array. You can set whatever default you want as long as it matches the types that you declared in the interface.
Next up, we are creating the slice itself with createSlice
. Here we can give it a name and pass through the initial state that we just defined. We then need to create reducers that allow us to manipulate the state. These reducers are then exported as actions with cartSlice.actions
.
Finally, we export cartSlice.reducer
so we can pass this into store.ts
.
Create Case Reducers/Actions in Redux Slice
I must admit the terminology around reducers and actions does confuse me. For the longest time I was used to actions being the functions that allow us to manipulate state. Now with createSlice
we’re passing in case reducers as the functions that allow us to manipulate state. Case reducers or actions?
So, case reducer functions defined in the reducer argument in createSlice
will have a corresponding action creator and will be included in the slice’s actions
field. Whilst this is a cumbersome answer it helps me realise how closely linked the two terms are. For me, it’s more useful to think of these functions as actions as it’s more accurately descriptive.
In my example, I want to create a few actions to manage the cart state and one of them is to add a line item. We can write the function directly in cartSlice
but I have abstracted the functions so I could refactor them to a different file if the number of functions grows too large.
You can see addLineItemReducer
takes two arguments: state
and action
. state
simply allows the function to access the data currently held in this state container. action
is a typed PayloadAction
and essentially holds any data that we want to pass into this function. In this example, we will have a LineItem
that we pass (as an argument) into this function when we call it.
The first thing I want to do is check if the LineItem
we passed in the action
payload already exists in our state. I do this using .findIndex
which returns -1 if it is not in the array else it gives us the index of the LineItem
in our state array. The rest of the logic is to add the LineItem
to the state array if we don’t already have it and if we do have it then update the quantity for that LineItem
in the state.
Now let’s flesh out the previous example of cartSlice
with our addLineItemReducer
.
You can see we have added it to createSlice
as addLineItem
and we export this action function to use in other components.
Use Typed Hooks in Components
We can use our recently created useAppDispatch
hook to dispatch the addLineItem
action in a functional component of our choice.
Import useAppDispatch
and addLineItem
. Next, instantiate useAppDispatch
:
const dispatch = useAppDispatch()
To dispatch addLineItem
and add a specific line item to the cart state we can use something like the following line of code where lineItem
is a LineItem
object.
dispatch(addLineItem(lineItem))
If you want to access the Redux store and the cart reducer/state then you can import useAppSelector
and use it like so.
const cartState = useAppSelector((state) => state.cart)
In our example cartState
only holds an array of LineItem
and we can access this by cartState.lineItems
.
Finishing up my Cart Slice
I wanted to add a few more reducers/actions to my cartSlice
and here they are for your information.