- Introduce Redux and why it's used to manage state
- Understand Redux Terminology such as Store, Actions, and Reducers
- Add Redux to a project and configure a single global store for state management
- Refactor a Counter Component to use Redux
In preparation to learn Redux it's assumed that you have previous working knowledge of the following:
- Managing state with both useState or useReducer
- Understanding how data is passed between components in React, possibly even have worked with useContent
- Working knowledge of switch statements
Before we jump into Redux let's take a minute to answer a few questions regarding our current knowledge of React so we can build off that as we learn about Redux.
❓ What are some of the differences between useState and useReducer?
❓ When would you opt to use useReducer over useState?
❓ When would you choose to use useContent and useReducer overuseState and provide a use case?
Redux was created as a state management tool for large applications in order to manage complex versions of state and increasing complexity of the business logic.
If your learning about Redux then it's safe to assume that you are already familiar with managing state using useState, useReducer and even useContext to create a global state.. This familiarity is important as Redux makes use of both the reducer and context concepts.
Let's take a look at the documentation on the official Redux site.
As we can see in the image below, passing props further and further down the hierarchy may at some point become confusing and/or unmanageable. Redux help mitigate this by providing a central store that component consume and pull only the data they need.
Redux brings with it some new terminology so let's review these new terms first before we implement it.
The store is the single source of truth our app and encapsulates the application data for the entire app program.
Actions are simply JavaScript objects that describe what change is being requested and includes any data relative to that change. The only requirement is that the object must contain a key called type.
Any additional key/value pairs that are included are completely optional and dependent on the need of the app's business logic.
Here is an action object that intends to increment the counter by 1.
{ type: 'INCREMENT' }
Although this meets the min requirements of an action we can extend it to include additional values. So perhaps we want to control by how much the value is incremented. For that we can add a new key called value.
{ type: 'INCREMENT', value: 1 }
We can continue that logic and create an action to decrement the counter by 1.
{ type: 'DECREMENT', value: 1 }
When an action gets dispatched it is sent to a Reducer to perform the actions needed as they were defined in type. The Reducer is a pure function that describes how the action will update the store (aka state object)
The Reducer always take the previous state and the action that was dispatched and then returns a brand new state.
The body of the function is always a switch statement that evaluates the type property in the action and executes the needed business logic.
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count += 1
}
case 'DECREMENT':
return {
count: state.count -= 1
}
}
}
Now that we have a basic understanding of how actions and reducers are used let's jump into setting up Redux.
For this demo we will be using the following starter code: CodeSandbox - Redux Counter - Starter
Here are the steps we will follow to configure and use Redux:
- Install redux packages (react-redux & redux)
- Create a reducer
- Add Redux to our App by importing createStore from redux
- Create a store and pass it a reducer
- Provide the state using a Provider
- Dispatch Actions from the child components
- Subscribe a component to the global store using connect
Once that is all done our app will look like this: CodeSandbox - Redux Counter - Solution
For the ease of the lecture the following libraries have already been installed in the CodeSandbox starter code.
- react-redux
- redux
Reducers receive an action and update state based on the action value . Large applications often have more than one reducer so it's a good idea to create a folder and place all your reducers there.
Let's create a reducers folder and then create a file called counterReducer.js
Let's create a bare bones counterReducer function that simply returns state as an object with a key of count.
const counterReducer = (state = 0, action) => {
return {
count: state
}
}
export default counterReducer
Now let's add the logic needed to update state based on one of the following actions that align with the existing handler functions.
- INCREMENT
- DECREMENT
- RESET
A switch statement is used to implement the conditional logic.
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count += 1
}
case 'DECREMENT':
return {
count: state.count -= action.value
}
case 'RESET':
return {
count: state.count = action.value
}
default:
return {
count: state
}
}
}
export default counterReducer
Adding Redux to the app means creating a new store. Since the App is the top level Component we will be creating our global store in App.
A store is used to hold one global state object. There is only one single store for the entire application. The store is responsible for managing state which includes performing state updates based on the actions it receives.
Creating a store requires that we first import createStore from redux.
import { createStore } from 'redux'
The store will also need to be passed a reducer so let's import the counterReducer function as well.
import counterReducer from './reducers/counterReducer'
Now let's create our global store and pass it the counterReducer we created earlier.
const store = createStore(counterReducer);
Once that's done, we can check the current state that has been initialized via our reducer by using the .getState() method.
console.log("Current State:", store.getState());
=> Current State: {count: 0}
You will often see the words state and store used interchangably. Technically, the state is the data, and the store is where it’s kept.
Now in order to provide access to the store throughout our entire React app will need to do so via the Provider.
Lets import the Provider from react-redux.
import { Provider } from 'react-redux'
Now we will wrap our component in the redux Provider and pass in the store.
<Provider store={store}>
<div>
<Counter />
</div>
</Provider>
Let's take a look at React Dev Tools and we should see the provider.
❓ Can you think of other instance where you wrapped a component like so and saw a reference to Provider in React Dev Tools?
Answer
React Router
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById("root")
);
useContenxt Hook
<DataContext.Provider value={userData}>
<ComponentA />
<ComponentE />
</DataContext.Provider>
To update state we need to dispatch an action to our store. For this the store object provides a dispatch() method.
Let's test calling dispatch and increment count by 1.
const store = createStore(counterReducer);
store.dispatch({type:"INCREMENT", value: 1})
console.log("Current State:", store.getState());
You should see the following in the console:
Current State: {count: 0}
Current State: {count: 1}
We now need to connect the Component to the store. For that we will import the connect method into the Counter Component as it will consume the provided store data.
import { connect } from 'react-redux';
The connect() method is a higher order function which is a fancy way of saying it returns a function when you call it. The function it returns will be passed the Counter component.
Essentially this returns an entirely new Component altogether.
export default connect()(Counter);
If we take a look at React Dev Tools now we will be able to see that it is being passed down a dispatch function.
It is however not yet being passed the count value in our global state so let's wire that up now.
In order to access the state in the the store we need to pass connect() a function that will provide access to the values we need. It essentially maps the values in state and passes them as props to the Component.
One thing to note is that the function needs to be created outside of the Counter Component so that it's within the scope of connect
function Counter(props) {
// Counter component code
}
const mapStateToProps = (state) => {
return {
count: state.count
}
}
Now pass connect the function.
export default connect(mapStateToProps)(Counter);
If we take a look at React Dev Tools now we will be able to see that props contains count.
Since dispatch() is being passed down via props we will use that method to pass the type of action to be performed along with the value.
const handleIncrement = () => {
props.dispatch({type: "INCREMENT", value: 1})
};
const handleDecrement = () => {
props.dispatch({type: "DECREMENT", value: 1})
};
const handleReset = () => {
props.dispatch({type: "RESET", value: 0})
};
We can take this even one step further and replace those supporting handler methods and map them directly to dispatch.
Add a new function called mapDispatchToProps outside of the Counter Component.
const mapDispatchToProps = dispatch => {
return {
handleIncrement: () => dispatch({type: "INCREMENT", value: 1}),
handleDecrement: () => dispatch({type: "DECREMENT", value: 1}),
handleReset: () => dispatch({type: "RESET", value: 0})
}
};
Since connect is what does the binding we need to pass the function to it as well.
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter);
And lastly update the buttons to reference these new props.
<button onClick={props.handleIncrement}>+</button>
<button onClick={props.handleDecrement}>-</button>
<button onClick={props.handleReset}>Reset</button>
Redux has a great Redux DevTools chrome extension.
First we must download and install it and then update createstore with the following in order to use it.
App.js
In App.js add the following to the store:
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
Now open dev tools and you should see a new Redux tab. If you click on that you will see the following.
In the Redux App click the Action tab and work with the app for a bit. As you click any of the buttons you will see actions being added and the info about what the action contained.