tutorial // Mar 24, 2021
How to Use Redux to Manage State
How to use Redux as a global store for managing application state. Learn how to interact with and manage your Redux store in a React-based UI using both class-based components and functional components via hooks.
![How to Use Redux to Manage State](/default_tutorial_seo_graphic.webp)
Getting Started
For this tutorial, we'll be using the CheatCode Next.js Boilerplate as a starting point. The paths shown above code blocks below map to this tutorial's repo on Github. To access that repo, click the "View on Github" button above (note: a CheatCode Pro subscription is required to access the repos for tutorials on CheatCode).
To get started, clone a copy of the Next.js Boilerplate from Github:
git clone git@github.com:cheatcode/nextjs-boilerplate.git
And then run:
cd nextjs-boilerplate && npm install
Next, optionally, if you're skipping the boilerplate or building as part of another app, you can install redux
and react-redux
:
npm i react react-redux
Understanding Data Flow in Redux
The purpose of Redux is to create a store (a place to keep your data) that can be accessed throughout your entire app. Typically, Redux is used to create a global store, or, a store that's accessible to your entire app (as opposed to a specific page or component).
const store = createStore();
When a store is created using the createStore()
function exported from the redux
package we installed above, it's passed another function known as a reducer. A reducer is responsible for deciding how to modify the current state contained in a store in response to some action taking place.
const store = createStore((state = {}, action) => {
switch (action.type) {
case "LOGIN":
return {
...state,
authenticated: true,
user: action.user,
};
case "LOGOUT":
return {
...state,
authenticated: false,
user: null,
};
default:
return {
...state,
};
}
}, {});
Here, we've passed an example reducer function to createStore()
. There are a few things to pay attention to here.
First, we want to notice that a reducer function takes two arguments: state
and action
(the state = {}
syntax here is us setting a default for state
in the event that its value is null or undefined).
The state
argument here contains the current state of the Redux store. The action
argument contains the current action being dispatched that will make changes to the state of the store.
Now, where things get interesting—and likely confusing—is when we start to modify our state based on an action. The syntax that likely looks weird here is the switch() {}
part (known technically in JavaScript as a case-switch statement):
(state = {}, action) => {
switch (action.type) {
case "LOGIN":
return {
...state,
authenticated: true,
user: action.user,
};
case "LOGOUT":
return {
...state,
authenticated: false,
user: null,
};
default:
return {
...state,
};
}
}
Here, we've extracted the reducer function from above for the sake of clarity (same exact code). The first part we want to look at is the switch (action.type) {}
. What this is saying is "take in the action.type
and try to find a match for it in this statement."
This is how a case-switch statement works. The idea is that, given some value (action.type
in this case), try to find a case
statement whose own value is equal to the value passed to the switch
.
So, here, if we assume that the value stored in action.type
is equal to "LOGOUT"
, the second case
statement here—case "LOGOUT"
—will match and the code following the :
colon after the case
will be executed.
What is that default part?
Like the name suggests, the default case above is a "fallback" value to return in the event that none of the other case statements match. Here, we just return a copy of the existing state but don't make any changes (technically we can skip the
...state
and returnstate
directly, but we've left it as-is for consistency).
In this example, we're returning a JavaScript object which will represent the updated copy of the state. We say that it's updated because the value we return from our switch—and ultimately our reducer function—is a copy of the original state (remember, this is the first argument passed to our reducer function). We say that it's a copy because here, we're using the ...state
syntax which is known as spread syntax in JavaScript.
const state = { food: 'Apple', animal: 'Red Panda' };
console.log(state);
// { food: 'Apple', animal: 'Red Panda' }
const newState = {
...state,
animal: 'Turkey',
};
console.log(newState);
// { food: 'Apple', animal: 'Turkey' }
console.log(state);
// { food: 'Apple', animal: 'Red Panda' }
Spread syntax allows us to "unpack" one object onto another. A good analogy for this is when you take a suitcase of your clothes to a hotel and unpack them into the drawers in your hotel room. Here, the suitcase is state
and the ...
before it is us "unzipping, unpacking, and moving our clothes into the hotel drawers."
The end result of this is that we get a new object (the one we're unpacking our existing object on to). From there, we can modify specific values in the object by adding additional properties underneath the ...state
.
So, what we accomplish here is taking what we had before, creating a copy of it, and then modifying specific properties on that object relative to the action being taken.
Zooming back out, then, we can see that the goal of our reducer function in Redux is to modify state in response to some action. If our action.type
was LOGOUT
, we know that we want to modify the state to reflect that the current user (as represented in the current state of the store) is logged out.
In the above example, then, we create a copy of the current state
and then set authenticated
to false
and user
to null
. Because we're returning an object here, as part of the switch()
statement's behavior, that return value will "bubble up" to the body of our reducer function and be returned from the reducer function. Whatever is returned from the reducer function, then, becomes the new state for the store.
Defining a Store for Global State
Let's get a little more concrete with this. Next, we're going to create a global store for our app that's going to hold some items for a shopping cart. Later, we'll create a React component for the cart where we'll dispatch events to the global store from.
To start, let's create our global store inside of the boilerplate we cloned earlier:
/lib/appStore.js
import { createStore } from "redux";
const appStore = createStore((state = {}, action) => {
// We'll define the functionality for our reducer here.
}, {
cart: [],
});
export default appStore;
Similar to what we learned about earlier, we're creating a Redux store for our app using the createStore()
method imported from the redux
package (included in the boilerplate you cloned, or if you opted, installed manually earlier).
Here, instead of using the generic name store
for the variable storing our store, we're using the name appStore
to reflect its contents (global state for our entire app). If we skip down to the bottom of the file, we'll see that we export default appStore
. This will come in handy later when we connect our store to our main <App />
component.
Code Tip
You're likely seeing a pattern emerge here. With Redux, we can have as many stores as we like. As we hinted at earlier, typically we'll see a single global store like we're setting up here but if it helps to separate state into multiple stores, go for it!
One big change we've made to the code we saw earlier is that we're passing another argument to our createStore()
call. As a second argument (in addition to our reducer function), we're passing a JavaScript object that represents the default state of our store. Though we don't have to do this, this is a convenient way to initialize your store with data.
Defining a Reducer for Your Global State Store
Next, we need to build out our reducer function to decide what happens when our store receives an action:
/lib/appStore.js
import { createStore } from "redux";
const appStore = createStore(
(state = {}, action) => {
switch (action.type) {
case "ADD_TO_CART":
return {
...state,
cart: [...state.cart, action.item],
};
case "REMOVE_FROM_CART":
return {
...state,
cart: [...state.cart].filter(({ _id }) => {
return _id !== action.itemId;
}),
};
case "CLEAR_CART":
return {
...state,
cart: [],
};
default:
return {
...state,
};
}
},
{
cart: [],
}
);
export default appStore;
Taking what we learned earlier and applying it, here we've introduced a case-switch statement that takes in an action.type
and defines a series of case
statement to decide what changes we'll make (if any).
Here, we've defined four case
statements and one default
case:
ADD_TO_CART
thetype
of the action when a user adds an item to their cart.REMOVE_FROM_CART
thetype
of the action when a user removes an item from their cart.CLEAR_CART
thetype
of the action when a user clears all of the items in their cart.
For each case
, we're using a similar pattern to what we saw earlier. We return a JavaScript object containing a copy of our existing state
and then make any necessary modifications.
Because we're building a shopping cart, the value that we're focused on is items
which contains, predictably, the items currently in the cart.
Looking at the ADD_TO_CART
case, we create a copy of our state and then set the cart
property equal to an array containing the existing state.cart
(if any) to the array. Next, we anticipate that our action
will pass an item
in addition to our type and concatenate or append that item to the end of the array. The end result here is that we take the existing items in the cart and we add the new one on the end.
Applying this same logic to the REMOVE_FROM_CART
case, we can see a similar approach being taken, however this time, our goal is not to add an item to the cart
array, but to remove or filter one out. First, we create a copy of our existing items into a new array and then use the JavaScript filter method to say "only keep the item we're currently looping over if its _id
property does not equal the itemId
we anticipate being passed with the action
."
For the CLEAR_CART
case, things are a bit simpler; all we care to do here is completely empty the cart
array. To do it, because we don't care to retain any of the items, we can just overwrite cart
with an empty array.
Using a Redux Provider to Access State in Your React App
Now that we have our Redux store set up and have our reducer planned out, now, we need to actually put our store to use.
The first option we'll look at for doing this is to use the <Provider />
component from the react-redux
package. This is an official package that offers helpers for using Redux in a React-based UI.
To use the <Provider />
, we need to place it at the top of our component tree. Typically, this is the component that's passed to our call to ReactDOM.render()
or ReactDOM.hydrate()
. For this tutorial, because we're using the CheatCode Next.js Boilerplate, we're going to place this in the pages/_app.js
file which is the main component rendered by Next.js and represents the "top" of our component tree.
/pages/_app.js
import React from "react";
import PropTypes from "prop-types";
import Head from "next/head";
import { Provider as ReduxProvider } from "react-redux";
import { ApolloProvider } from "@apollo/client";
import Navigation from "../components/Navigation";
import loginWithToken from "../lib/users/loginWithToken";
import appStore from "../lib/appStore";
import client from "../graphql/client";
import "../styles/styles.css";
class App extends React.Component {
state = {
loading: true,
};
async componentDidMount() {
[...]
}
render() {
const { Component, pageProps } = this.props;
const { loading } = this.state;
if (loading) return <div />;
return (
<React.Fragment>
<Head>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>App</title>
</Head>
<ReduxProvider store={appStore}>
<ApolloProvider client={client}>
<Navigation />
<div className="container">
<Component {...pageProps} />
</div>
</ApolloProvider>
</ReduxProvider>
</React.Fragment>
);
}
}
App.propTypes = {
Component: PropTypes.object.isRequired,
pageProps: PropTypes.object.isRequired,
};
export default App;
A few notes here. First, the CheatCode Next.js Boilerplate uses Redux as a global store by default. It also uses the <Provider />
component to hand down the store to the component tree.
Here, to make our work clear, we're going to change two big things:
- Replace the
import store from '../lib/store'
withimport appStore from '../lib/appStore'
. - Down in the
render()
method of the<App />
component, replace the name of the variable being passed to thestore
prop on the<ReduxProvider />
component to beappStore
.
Of note, when we import the <Provider />
component from the react-redux
package, we also rename it to <ReduxProvider />
to help us better understand what type of provider it is (use of the name Provider
is common in React libraries so doing this helps us to avoid namespace collisions and understand the intent of each Provider
).
By doing this, though it may not look like much, what we've accomplished is giving any component in our app access to the appStore
that we passed as the store
prop on the <ReduxProvider />
component. If we didn't do this, the only way we'd be able to access the store would be to import it directly into our component files (we'll take a look at this pattern later).
<ReduxProvider store={appStore}>
[...]
</ReduxProvider>
Next, we're going to see how to access the store from within a component in our tree using three different methods: accessing the store in a component via the react-redux
connect
HOC (higher-order component), via functional component hooks, and via the direct import method that we just hinted at.
Accessing Your Store in a Class-Based React Component with Redux Connect
Like we discussed earlier, our goal is to create a shopping cart to demonstrate our global store. Before we build out our cart, though, we need some items that we can add to our cart. To showcase the usage of the connect
HOC from react-redux
, we'll build our storefront as a class-based React component.
To get started, let's modify the /pages/index.js
component in the CheatCode Next.js Boilerplate to give us a simple list of items we can add or remove from our cart:
/pages/index.js
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import StyledStorefront from "./styles";
const storefrontItems = [
{
_id: "turkey-sandwich",
image: "https://loremflickr.com/640/480/turkeysandwich",
title: "Turkey Sandwich",
price: "$2.19",
},
{
_id: "potato-chips",
image: "https://loremflickr.com/640/480/potatochips",
title: "Potato Chips",
price: "$1.19",
},
{
_id: "soda-pop",
image: "https://loremflickr.com/640/480/popcan",
title: "Soda Pop",
price: "$1.00",
},
];
class Index extends React.Component {
render() {
const { cart, addToCart, removeFromCart } = this.props;
return (
<StyledStorefront>
<ul>
{storefrontItems.map((item) => {
const { _id, image, title, price } = item;
const itemInCart =
cart && cart.find((cartItem) => cartItem._id === _id);
return (
<li key={_id}>
<img src={image} alt={title} />
<header>
<h4>{title}</h4>
<p>{price}</p>
<button
className="button button-primary"
onClick={() =>
!itemInCart ? addToCart(item) : removeFromCart(_id)
}
>
{!itemInCart ? "Add to Cart" : "Remove From Cart"}
</button>
</header>
</li>
);
})}
</ul>
</StyledStorefront>
);
}
}
Index.propTypes = {
cart: PropTypes.array.isRequired,
addToCart: PropTypes.func.isRequired,
removeFromCart: PropTypes.func.isRequired,
};
export default connect(
(state) => {
return {
cart: state.cart,
};
},
(dispatch) => {
return {
addToCart: (item) => dispatch({ type: "ADD_TO_CART", item }),
removeFromCart: (itemId) =>
dispatch({ type: "REMOVE_FROM_CART", itemId }),
};
}
)(Index);
A lot to look at here but let's start at the bottom with the connect()
call. This connect()
method is being imported at the top of our /pages/index.js
file. Like the name implies, the connect()
method connects the component we're writing to the Redux store. More specifically, it takes the store that we passed to the <ReduxProvider />
and maps its state and dispatch method to the component we're wrapping.
In this example, we're wrapping our <Index />
component with the connect()
so that we can connect our storefront UI to the Redux store.
If we look a bit closer, the connect()
method takes two arguments:
- First, a function that's referred to as
mapStateToProps
which allows us to access the current state of the Redux store and map its contents to the props of the component we're wrapping (i.e., allow us to selectively pick out what data from state we want to give our component access to). - Second, a function that's referred to as
mapDispatchToProps
which allows us to access thedispatch
method for the Redux store within our component.
Looking at mapStateToProps
, the idea here is pretty straightforward: define a function that receives the current state
of the Redux store as an argument and then return a JavaScript object containing the names of props we want to expose to our component. Now, look close. What we're doing here is saying "we want to take the state.cart
value and map it to the cart
prop on our component.
By doing this, now, inside of our render()
method (and other lifecycle methods on the component), we're able to say this.props.cart
, or, if we're using destructuring const { cart } = this.props;
.
What's neat about this is that as our store updates, now, this.props.cart
will update, too. The advantage here is that what we get is essentially a real-time update in our UI.
Looking at the second argument passed to connect()
, again, we have another function called mapDispatchToProps
. This is nearly identical to the mapStateToProps
function, except that it takes in a single argument dispatch
which is a function itself. This function is used to dispatch actions (remember those?) to our store.
Remember earlier how we had the case-switch statement with stuff like case "ADD_TO_CART"
? This is where we connect that stuff with our user interface. Here, in our mapDispatchToProps
function, what we're doing is trying to pass props down to our component (the one wrapped by our call to connect()
) that represent the different actions we're trying to dispatch.
Here, we're passing down two props: addToCart
and removeFromCart
. We're setting these props equal to a function which expects to be passed either an item
or an itemId
(respectively).
When the addToCart
function is called like this.props.addToCart({ _id: '123', title: 'Item Title', ... })
what's happening is that the object passed to addToCart
is handed back up to this function being set to the addToCart
prop and then handed off to a call to the dispatch
method on our Redux store.
If we take a look at that call to dispatch()
, we can see that we pass an object here, too, but this time we add a type
property. Look familiar? Yes, the type: "ADD_TO_CART"
maps back to the case "ADD_TO_CART"
that we saw in our reducer function in /lib/appStore.js
!
Making sense?
The same thing applies here with removeFromCart
, however, when we call it, instead of passing an entire item to add to the cart, we just pass the itemId
or the _id
from the item object.
To make this more clear, let's take a look at the render()
method of our component.
/pages/index.js
class Index extends React.Component {
render() {
const { cart, addToCart, removeFromCart } = this.props;
return (
<StyledStorefront>
<ul>
{storefrontItems.map((item) => {
const { _id, image, title, price } = item;
const itemInCart =
cart && cart.find((cartItem) => cartItem._id === _id);
return (
<li key={_id}>
<img src={image} alt={title} />
<header>
<h4>{title}</h4>
<p>{price}</p>
<button
className="button button-primary"
onClick={() =>
!itemInCart ? addToCart(item) : removeFromCart(_id)
}
>
{!itemInCart ? "Add to Cart" : "Remove From Cart"}
</button>
</header>
</li>
);
})}
</ul>
</StyledStorefront>
);
}
}
This should be making more sense. Notice at the top of this file we're using destructuring to "pluck off" the cart
(that we mapped from state in mapStateToProps
), addToCart
(that we added to props in mapDispatchToProps
), and removeFromCart
(that we added to props in mapDispatchToProps
).
Putting all of those to use, first, we use the static array of storefrontItems
that we saw above and map over it (these are just made up items mimicking what we might get back from a database).
As we map over each item, we want to ask the question "has this item already been added to the cart?"
This is where the variable itemInCart
comes into play within our .map()
method. Here, we're assigning the variable to a call to cart.find()
. .find()
is a native JavaScript function that allows us to call a function which attempts to find a matching element in some array.
Here, we want to see if we can find a JavaScript object in our cart
array with an _id
property equal to the _id
of the storefront item that's currently being looped over in our map.
If we find a match? That means the item is in our cart!
Next, utilizing this value, we do two things involving the "Add to Cart" button down below. First, we assign an onClick
handler to say "when this button is clicked, either add this item to the cart or, if it's already in the cart, remove it." Notice that here we're calling the addToCart()
and removeFromCart()
functions that we mapped over to props in our mapDispatchToProps
function earlier.
Remember that depending on what we're doing—adding an item to the cart or removing an existing one—we're going to pass different data to dispatch
.
That's one part down! Now, if you click the "Add to Cart" button for each item, you should see it flip to "Remove From Cart" and vice versa if you click it again!
Accessing Your Store in a Functional React Component with Redux Hooks
Another method for accessing a Redux store in React is to use one of the hooks implementations included in the react-redux
package. Hooks are a convention in React for handling state within functional components or responding to side effects of changes to props or state in a functional component.
In react-redux
, one of the hooks available to use is called useSelector()
. It allows us to directly "select" a value (or values) from our Redux store.
As an example, we're going to update the <Navigation />
component in the CheatCode Next.js Boilerplate to include a cart items count (with a link to the cart page we'll build next) that updates automatically as items are added or removed from our cart.
/components/Navigation/index.js
import React, { useState, useEffect } from "react";
import { useRouter } from "next/router";
import { useSelector } from "react-redux";
import NavigationLink from "../NavigationLink";
import Link from "next/link";
import StyledNavigation from "./styles";
const Navigation = () => {
const cart = useSelector((state) => state.cart);
const router = useRouter();
const [navigationOpen, setNavigationOpen] = useState(false);
const handleRouteChange = () => {
setNavigationOpen(false);
};
useEffect(() => {
router.events.on("routeChangeStart", handleRouteChange);
return () => {
router.events.off("routeChangeStart", handleRouteChange);
};
}, []);
return (
<StyledNavigation className={`navigation ${navigationOpen ? "open" : ""}`}>
<div className="container">
<Link href="/" passHref>
<a className="brand">BigBox</a>
</Link>
<i
className="fas fa-bars"
onClick={() => setNavigationOpen(!navigationOpen)}
/>
<div className="navigation-items">
<ul>
<NavigationLink href="/">Storefront</NavigationLink>
</ul>
<p className="cart" onClick={() => router.push("/cart")}>
<i className="fas fa-shopping-cart" /> {(cart && cart.length) || 0}{" "}
Cart
</p>
</div>
</div>
</StyledNavigation>
);
};
Navigation.propTypes = {};
export default Navigation;
This looks quite a bit different. The big change we're making here is that instead of using a class-based component we're using a functional component. This is a technique for defining a React component that's simpler in nature. Functional components are components that do not need the lifecycle methods and structure of a JavaScript class.
To fill the gap between the missing lifecycle methods and the occasional need for access to state, in version 16, React introduced hooks. A way to get access to component-level state without having to introduce the full weight of a class-based component.
Our navigation fits this need quite well. It relies on some simple state setting and data fetching, but doesn't need much more than that; a great fit for functional components and hooks.
Here, the thing we want to pay attention to is our call to useSelector()
near the top of our component. This is being imported from the react-redux
package and is responsible for helping us to pluck off some value from our state (a similar concept to what we saw with mapStateToProps
in our storefront).
The way the hook works is that it takes in a function as an argument and when our component renders, that function is called, receiving the current state of our Redux store.
Wait? What Redux store? The one we passed via our <ReduxProvider />
. Though we can't see it, behind the scenes, the useSelector()
hook here checks for an existing Redux store in the props of our component tree. If it finds one, the call succeeds and we're returned the value we requested from state
(assuming it exists on state).
If we did not have our <ReduxProvider />
higher up in our component tree, we'd get an error from React saying that the useSelector()
hook requires access to a store and that we need to set up a provider.
From here, things are pretty self-explanatory. We take the retrieved state.cart
value, placing it in our cart
variable and then toward the bottom of our component, render the current length
of the cart
array.
That's it! Though it may not look like much, go back to the storefront page and add some items to the cart. Notice that even though we're dispatching our addToCart
or removeFromCart
actions from the storefront, changes to the Redux store propagate to any other component in our application that retrieves and listens for changes to data in our Redux store.
This is the magic of Redux at play. You can change data from one place and have those changes automatically reflected in another place. With a feature like a shopping cart, this is a great way to add visual feedback to users that the action they performed succeeded without the need for things like popup alerts or other jarring user interface elements.
Accessing Your Store Directly in a Class-Based React Component
Now that we've seen the two most common methods for accessing a Redux store, let's look at one more. In our final example, we're going to wire up a page for our cart, render the items in the cart, and give ourselves the ability to remove one item at a time, or, clear the cart entirely.
/pages/cart/index.js
import React from "react";
import appStore from "../../lib/appStore";
import StyledCart from "./styles";
class Cart extends React.Component {
state = {
cart: [],
};
componentDidMount() {
this.handleStoreStateChange();
this.unsubscribeFromStore = appStore.subscribe(this.handleStoreStateChange);
}
componentWillUnmount() {
this.unsubscribeFromStore();
}
handleStoreStateChange = () => {
const state = appStore.getState();
this.setState({ cart: state && state.cart });
};
render() {
const { cart } = this.state;
return (
<StyledCart>
<header>
<h1>Cart</h1>
<button
className="button button-warning"
onClick={() =>
appStore.dispatch({
type: "CLEAR_CART",
})
}
>
Clear Cart
</button>
</header>
{cart && cart.length === 0 && (
<div className="blank-state bordered">
<h4>No Items in Your Cart</h4>
<p>To add some items, visit the storefront.</p>
</div>
)}
{cart && cart.length > 0 && (
<ul>
{cart.map(({ _id, title, price }) => {
return (
<li key={_id}>
<p>
<strong>{title}</strong> x1
</p>
<div>
<p className="price">{price}</p>
<i
className="fas fa-times"
onClick={() =>
appStore.dispatch({
type: "REMOVE_FROM_CART",
itemId: _id,
})
}
/>
</div>
</li>
);
})}
</ul>
)}
</StyledCart>
);
}
}
export default Cart;
What we want to pay attention to here is that if we look at our imports at the top of our file, we're no longer importing any functions from the react-redux
package.
Instead, here, we're pulling in our appStore
directly.
What's cool about Redux is that it's fairly versatile. While we can use helpful tools like the connect()
method or the useSelector()
hooks, we can access our store all the same directly.
The advantages to this method are control, clarity, and simplicity. By accessing your store directly, there's no confusion about how the store is finding its way to our component (e.g., using the <ReduxProvider />
) and we remove the need for additional code to map us to what we want.
Instead, we just access it!
Above, once we've imported our appStore
, we want to look at three methods defined on our Cart
class: componentDidMount()
, componentWillUnmount()
, and handleStoreStateChange()
.
The first two methods, componentDidMount()
and componentWillUnmount()
are built-in lifecycle methods in React. Like their names imply, these are functions that we want to call either after our component has mounted in the DOM (document object model, or, the in-memory representation of what's rendered on screen to users), or, right before our component is going to be unmounted from the DOM.
Inside of componentDidMount()
, we're doing two things: first, we're making a call to this.handleStoreStateChange()
. Let's ignore that for a second.
Next, we're assigning this.unsubscribeFromStore
to the result of calling appStore.subscribe()
. What is this?
In Redux, a subscription is a way to register a callback function that's fired whenever a change is made to our store. Here, we're calling to appStore.subscribe()
passing in this.handleStoreStateChange
. That function is responsible for updating our <Cart />
component whenever a change is made to our store.
If we look at handleStoreStateChange()
, we'll see that it does two things: first, it calls to the .getState()
method on our appStore
store to get the current state of our Redux store. Next, because all we care about in this view are the items in our cart, it takes the state.cart
value and then copies it over to the state of the <Cart />
component.
This allows us to accomplish something similar to what we saw in the previous section with useSelector()
, but instead of directly accessing values via the hook, we access the current state of the entire store first with .getState()
and then pluck off what we want. We use the React class-based component's state
(this.state
) as our mechanism for rendering data.
When using this method, there's one gotcha: how do we set the initial this.state
value for our <Cart />
component. This is where the call to this.handleStoreStateChange()
in componentDidMount()
comes in handy.
Here, we're saying "when the component mounts, go and get the current state of the store and pop it on to the <Cart />
component's state." This ensures that whether we're just loading up the cart page for the first time, or, we're receiving changes after the mount, our component's state is properly updated.
Conversely, when our component is going to unmount from the DOM (meaning we're leaving the page), we call this.unsubscribeFromStore()
which contains the function we received back from our appStore.subscribe()
method earlier. This function, when called, stops the listeners for the store, removing them from memory. This is known as a "clean up" to ensure that we don't have unnecessary code running in the background for pages that are no longer on screen for the user.
Now that we have these pieces, down in our render()
method, we can close the loop on all of this:
/pages/cart/index.js
[...]
class Cart extends React.Component {
state = {
cart: [],
};
[...]
render() {
const { cart } = this.state;
return (
<StyledCart>
<header>
<h1>Cart</h1>
<button
className="button button-warning"
onClick={() =>
appStore.dispatch({
type: "CLEAR_CART",
})
}
>
Clear Cart
</button>
</header>
{cart && cart.length === 0 && (
<div className="blank-state bordered">
<h4>No Items in Your Cart</h4>
<p>To add some items, visit the storefront.</p>
</div>
)}
{cart && cart.length > 0 && (
<ul>
{cart.map(({ _id, title, price }) => {
return (
<li key={_id}>
<p>
<strong>{title}</strong> x1
</p>
<div>
<p className="price">{price}</p>
<i
className="fas fa-times"
onClick={() =>
appStore.dispatch({
type: "REMOVE_FROM_CART",
itemId: _id,
})
}
/>
</div>
</li>
);
})}
</ul>
)}
</StyledCart>
);
}
}
export default Cart;
Earlier, we learned about dispatching actions to our Redux store using the named functions we created and mapped to our storefront component's props with mapDispatchToProps
.
When we called to the dispatch
method (the one we received from the argument passed to the mapDispatchToProps
function), what we were technically doing is calling our appStore.dispatch
method.
Just like we saw before, this method is responsible for dispatching an action to our Redux store. The work we did with mapDispatchToProps
was purely for convenience. The convenience being that we were able to create a named function that represented the action being taken as opposed to passing a generic dispatch
prop to our component (which is potentially more confusing).
Here, instead of using a mapDispatchToProps
, we go commando and just use appStore.dispatch()
directly. What's cool here is that we're passing the same exact thing to appStore.dispatch()
as we did with addToCart()
and removeFromCart()
earlier. The difference this time is that we're just calling dispatch
directly.
If we try to remove an item from our cart now by clicking the "x" next to the item, or, click the "Clear Cart" button near the top of the page, our actions are dispatched and the cart
value on our Redux store is updated!
Wrapping Up
In this tutorial, we learned about three different methods for interacting with Redux, using two different types of component styles in React: class-based components and functional components.
Redux is a great way to handle global state in an app and add a bit of "real-time" style polish to your app. What's great about it is its flexibility, as we've seen here. We're not locked in to one way of doing things which means that Redux can adapt to both new and existing projects (React-based or otherwise) with ease.
If this post was helpful, please consider sharing it on one of the sites linked in the "Share This Tutorial" bar below and consider subscribing to CheatCode Pro to support the site and join our subscriber-only Discord chat!