M06 - State Management
Introduction
Usually, you will pass information from a parent component to a child component via props. But passing props can become verbose and inconvenient if you have to pass them through many components in the middle, or if many components in your app need the same information. When your React application is a very big and hierarcial, then it is very problematic to pass data via props to deep level child components and back to parents (I think you have feel it already).
Small example
Below example demonstrates how to send props from the parent App
to child Movie
components (movie title
and price
). Notice also how movie's total price is visible in App
components. App
components addPrice
function will be called when Buy
button has been clicked in Movie
component. So, you can call parents functions via props too.
This is ok when you have a small apps and not so big hierarcy.
Bigger example
Below example has a bigger hierarcy. App
component will hold Component A
and Component D
. And those will hold components B
and E
and yes those will hold components C
and F
. How we can send component C
's name
state/value to component F
?
Now you should see what is the problem with bigger applications to share information between components.
Redux
Redux is an one of the most used library for managing application state. You can use Redux together with React, or with any other view library. It is tiny (2kB, including dependencies), but has a large ecosystem of addons available.
When your React application is a big and very hierarcial, then it is very problematic to pass data via props to deep level child components and back to parents (I think you have feel it already). In those situations you should use some of the state libraries to collect all the data in one "global store". Redux maintains the state of an entire application in a single immutable state tree, which is used via actions and reducers.
User clicks/changes something in the UI or some other event occurs. Programmed action happens and it will be send to reducer with dispatcher function. Reducer changes states in the store and UI will be updated.
Workflow: UI/Event -> Action -> Dispatcher -> Reducer -> Store -> UI update
Note
Redux has three building parts: actions, store and reducers.
In Redux, you can change the state only by dispatching an action - an object describing what should happen. This, in turn, runs a function called a reducer, which, given an action object and previous state, returns a new state.
While the Redux model has been around for a while now, its biggest issue is the related boilerplate. Writing actions to describe every possible state change and then multiple reducers to handle those actions can lead to a lot of code, which can quickly become hard to maintain. That’s why Redux Toolkit was created. Nowadays, Redux Toolkit is the go-to way to use Redux. It simplifies the store setup, reduces the required boilerplate, and follows the best practices by default.
Redux Toolkit
Redux Toolkit was introduced with a purpose to be the standard way to write redux logic. By using a Slices, you can write all the code you need for your Redux store in a single file, including actions and reducers (multiple slices should be written in separated files).
Redux Toolkit's createSlice
is a higher order function which accepts an initial state, an object full of reducer functions and a slice name. It automatically generates action creators and action types that correspond to the reducers and state.
Writing Slices
Redux Toolkit has a createSlice
API that will help to simplify Redux reducer logic and actions. The main idea is to keep React application global state in here (variables which you want to connect from all of your JavaScript files). You will use reducer functions to modify your global state.
createSlice
takes an object with three main options fields:
name
: a string that will be used as the prefix for generated action typesinitialState
: the initial state of the reducerreducers
: an object where the keys are strings, and the values are "case reducer" functions that will handle specific actions
todosSlice.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Created reducer and actions will be exported to be available other JavaScripts codes in the project. Look more from the below examples.
Using configureStore
Redux Toolkit has a configureStore
API that simplifies the store setup process. configureStore
wraps around the Redux core createStore
API, and handles most of the store setup for us automatically. You need to import your slice(s) and create your store object. Use Provider
to wrap you global store around your App
and your global state is available from all of your application components.
index.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Use Redux State and Actions in React Components
You can use the React-Redux hooks to let React components interact with the Redux store. You can read data from the store with useSelector
and dispatch actions using useDispatch
.
App.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Example: Redux Toolkit Click Counter App
Let's create a small Click Counter App with Redux Toolkit and Slides.
Create a new project and add the Redux Toolkit and React-Redux packages to your project:
1 2 3 |
|
Below code defines a Redux Toolkit Slide which holds click counter app's count
and clicked
states. These states can be modified with increment
and decrement
reducers functions.
counterSlice.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Above Slide can be used for example in App.js
to dispatch actions to Slide.
App.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Redux store need to be defined for example in index.js
:
index.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
You can test application in codesandbox: redux-click-counter-app
Example: Redux Toolkit Todo App
This example shows how you can create a todo application which uses Redux Toolkit and Slides to store todos.
New Project
You will need to create a new project and install needed npm packages.
Below code defines a Redux Toolkit Slide which holds todo app's todos
state. This state can be modified with add
and remove
reducers functions.
todosSlice.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Above Slide can be used for example in App.js
to dispatch actions to Slide.
App.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
Redux store need to be defined for example in index.js
:
index.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
You can test application in codesandbox: redux-todo-app
Example: Redux Toolkit JSON based Highscores
This example shows how you can create a highscores application which uses Redux Toolkit and Slides to store highscores data. Highscores will be loaded from JSON when the application starts.
New Project
You will need to create a new project and install needed npm packages.
You will need to store below JSON data to your projects public
folder. We will use it later in this example.
public/highscores.json
1 2 3 4 5 6 7 8 9 10 |
|
Below code defines a Redux Toolkit Slide, which holds highscore app's highscores
state. This state can be modified with addHighscore
reducers functions. Redux Toolkit's createAsyncThunk
function will be used to create a callback function that returns a promise. This abstracts the standard recommended approach for handling async request lifecycles. We will use this with axios to load external JSON data, when application starts.
One of the key concepts of Redux is that each slice reducer "owns" its slice of state, and that many slice reducers can independently respond to the same action type. extraReducers
allows createSlice to respond to other action types besides the types it has generated. As case reducers specified with extraReducers
are meant to reference "external" actions, they will not have actions generated in slice.actions. NOW, extrareducers will be used to detect the status of the loading process with axios.
highscoreSlice.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
Above highscoreSlide
can be used for example in App.js
. Application will start to load highscores, if status
is idle
inside useEffect
function.
App.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Note
Remember configure your global store in index.js
.
You can test application in codesandbox: redux-highscore-app
Example: Bigger example with Toolkit
Below example has modified version of Bigger example in the beginning of this materials.
React Context - another way to handle app state
Context lets the parent component make some information available to any component in the tree below it—no matter how deep—without passing it explicitly through props. One common use case is to detect current logged user. Many components might need to know the currently logged in user. Putting it in context makes it convenient to read it anywhere in the tree.
There are four steps to using React context:
- Create context using the
createContext
method. - Take your created context and wrap the context provider around your component tree.
- Put any value you like on your context provider using the
value
prop. - Read that value within any component by using the context consumer.
Example: UserContext
The built-in factory function createContext(default)
creates a context instance. You can use useContext()
hook to update/modify the user data from the UserContext
.
You also need to define a UserProvider
, which is a Context.Provider
component available on the context instance and it is used to provide the context to its child components, no matter how deep they are.
In this example we will add a user
state to hold user response data from the server side (login information). We also create userLogin
and user userLogout
functions to change this user
state. Note, that we have one extra auth
value here (true/false).
src/context.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
We will wrap all the components that share data within the context provider as a parent component. This can be done in your project index.js
file to use above UserProvider
. Now your UserContext
will be available in all the components inside a App component (whole of our React app).
index.js | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Now you can add a logged user information with userLogin
function from UserContext
.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Stored data is available in UserContext
. Use useContext
React Hook that to read and subscribe to context from any of your application component.
some other .js file / component | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
You can find a completed demo from course examples: Todos.Auth.Example.
Final words
Now we only introduced only a few React state management libraries. You can find a lot of them from the internet, just make a google seach: react state management libraries.