Skip to content

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 types
  • initialState: the initial state of the reducer
  • reducers: 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
import { createSlice } from '@reduxjs/toolkit'

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    todos: []
  },
  reducers: {
    add(state, action) {
      // code to add todo
    },
    remove(state, action) {
      // code to remove todo
    }
  }
})

// get actions and reducer from todos slice
const { actions, reducer } = todosSlice

// export actions
export const { add, remove } = actions;

// export reducer
export default reducer;

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
///...
import { configureStore } from "@reduxjs/toolkit";
import { Provider } from "react-redux";
import todosReducer from './todosSlice'

const store = configureStore({
  reducer: todosReducer
});

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

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
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { add, remove } from './todosSlice'

export function App() {
  // read todos from store
  const { todos } = useSelector( state => state);
  // use dispatch to call reducer actions
  const dispatch = useDispatch();

  const todo = {
    "text": "Learn Redux",
    "id": 1
  }

  return (
    <div>
      <button onClick={() => dispatch(add(todo))}>Add</button>
      <button onClick={() => dispatch(remove(todo.id))}>Remove</button>
    </div>
  );
}

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
npx create-react-app redux-click-counter-app
cd  redux-click-counter-app
npm install @reduxjs/toolkit react-redux

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
// use createSlice from toolkit
import { createSlice } from '@reduxjs/toolkit';

// create todos slice
// generates action types from reducers: 
//   counter/increment, counter/decrement
// 1. define slice name
// 2. initialize state variables
// 3. create reducers functions
const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    count: 0, clicked: 0
  },
  reducers: {
    // param is not used in this example
    increment: (state, param) => {
      // modify states
      state.count++
      state.clicked++
    },
    // param is not used in this example
    decrement: (state, param) => {
      // modify states
      state.count--
      state.clicked++
    },
  }
});

// get actions and reducer from conter slice
const { actions, reducer } = counterSlice

// export actions
export const { increment, decrement } = actions;

// export reducer
export default reducer;

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
import React from 'react';
import { useDispatch, useSelector } from "react-redux";
// import actions from Slide
import { increment, decrement } from "./counterSlice";
import './App.css';

function Counter() {
  const { count } = useSelector( state => state)
  const dispatch = useDispatch()
  return (
    <div>
      <button onClick={() => dispatch(increment())}> INCREMENT </button>
      <button onClick={() => dispatch(decrement())}> DECREMENT </button>
      <p>Counter value: {count}</p>
    </div>
  );
}

function ShowClickedCount() {
  const { clicked } = useSelector( state => state)
  return (
    <p>Clicked count: {clicked}</p>
  );
}

function App() {
  return (
    <div className="App">
      <Counter />
      <ShowClickedCount />
    </div>
  );
}

export default App;

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
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import { configureStore } from "@reduxjs/toolkit";
import { Provider } from "react-redux";
import counterReducer from "./counterSlice";

const store = configureStore({
  reducer: counterReducer
});

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

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
// use createSlice from toolkit
import { createSlice } from '@reduxjs/toolkit';

// create todos slice
// generates action types from reducer functions: todos/add, todos/remove
// 1. define slice name
// 2. init state
// 3. create reducers functions
const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    todos: [],
  },
  reducers: {
    add: (state, action) => {
      // add a new todo
      state.todos.push(action.payload.todo)
    },
    remove: (state, action) => {
      // filter/remove one todo by id
      state.todos = state.todos.filter(todo => todo.id !== action.payload.id)
    } 
  }
});

// get actions and reducer from rodos slice
const { actions, reducer } = todosSlice

// export actions
export const { add , remove} = actions;

// export reducer
export default reducer;

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
import React, { useState } from 'react';
import { useDispatch, useSelector } from "react-redux";
import { add, remove } from "./todosSlice";
import { v4 as uuidv4 } from 'uuid'; // npm install uuid
import './App.css';

function Banner() {
  return (
    <h1>Todo Example with React</h1>
  )
}

function ToDoFormAndList() {
  const [todoText, setTodoText] = useState(""); 
  const { todos } = useSelector( state => state)
  const dispatch = useDispatch()

  const addTodo = (event) => {
    const todo = { id: uuidv4(), text: todoText };
    const payload = { todo: todo}
    dispatch(add(payload))
    setTodoText("")
  }

  const removeTodo = (id) => {
    const payload = { id: id}
    dispatch(remove(payload))
  }

  return (
    <div>
      <form>
        <input type='text' value={todoText} onChange={event => setTodoText(event.target.value)} placeholder="Write a new todo here" />
        <input type='button' value='Add' onClick={addTodo}/>
      </form>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            {todo.text+" "} <span onClick={() => removeTodo(todo.id)}> x </span>
          </li>
        ))}
      </ul>   
    </div>
  )  
}

function App() {
  return (
    <div>
      <Banner/>
      <ToDoFormAndList/>
    </div>
  );
}

export default App;

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
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import { configureStore } from "@reduxjs/toolkit";
import { Provider } from "react-redux";
import todosReducer from "./todosSlice";

const store = configureStore({
  reducer: todosReducer
});

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

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.

Image 01

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
{
  "highscores": 
  [
    { "name":"Jaakko Teppo","score":10000 },
    { "name":"Smith Fessel", "score":8000 },
    { "name":"Jones Bigss", "score":6000 },
    { "name":"Farren Goods", "score":4000 },
    { "name":"Milla Junes", "score":2000 }
  ]
}  

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
// https://redux-toolkit.js.org/api/createSlice
// https://redux-toolkit.js.org/api/createAsyncThunk

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

// 'highscores/fetchHigscores' is just a name
export const fetchHighscores = 
  createAsyncThunk('highscores/fetchHigscores', async () => {
    const response = await axios.get('highscores.json');
    return response.data.highscores;
  }
);

export const highscoreSlice = createSlice({
  name: 'highscores',
  initialState: {
    highscores: [],
    status: 'idle',
    error: null,
  },
  reducers: {
    addHighscore: (state, action) => {
      state.highscores.push(action.payload.highscore);
      state.highscores.sort((firstItem, secondItem) => secondItem.score - firstItem.score);
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchHighscores.pending, (state, action) => {
        state.status = 'loading';
      })
      .addCase(fetchHighscores.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.highscores.push(...action.payload);
      })
      .addCase(fetchHighscores.rejected,(state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      })      
  }
});

export const { addHighscore } = highscoreSlice.actions;

export default highscoreSlice.reducer;

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
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from "react-redux";
import { fetchHighscores, addHighscore } from "./highscoreSlice";
import './App.css';

function App() {
  const [name, setName] = useState('');
  const [score, setScore] = useState(0);
  const { highscores, status } = useSelector( state => state);
  const dispatch = useDispatch();

  useEffect(() => {
    console.log('status', status); // look Web browser's console!!
    if (status === 'idle') {
      dispatch(fetchHighscores());
    }
  }, [status, dispatch]);

  const add = () =>{
    const payload = { highscore: { name: name, score: score } };
    dispatch(addHighscore(payload));
  }

  return (
    <div className="App">
      name: <input type='text' placeholder='name' onChange={(e) => setName(e.target.value)}></input><br/>
      score: <input type='number' placeholder='score' onChange={(e) => setScore(e.target.value)}></input><br/>
      <button onClick={() => add()}>add</button>
      <ul>
        {highscores.map( (hs, index) => (
          <li key={index}> {hs.name} : {hs.score} </li>
        ))}
      </ul> 
    </div>
  );
}

export default App;

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
import React, { useState, createContext } from 'react';

export const UserContext = createContext();

const UserProvider = ({ children }) => {
  const [user, setUser] = useState(
    { username: '', id: '', token: '', auth: false }
  );

  const userLogin = (response) => {
    setUser(() => ({
      username: response.username,
      id: response.id,
      token: response.token,
      auth: true
    }));
  };

  const userLogout = () => {
    setUser(() => ({
      username: '',
      token: '',
      auth: false
    }));
  };

  return (
    <UserContext.Provider value={{ user, userLogin, userLogout }}>
      {children}
    </UserContext.Provider>
  );
}

export default UserProvider;

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
//...
import UserProvider from './context';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <UserProvider>
        <App />
      </UserProvider>
    </BrowserRouter>
  </React.StrictMode>
);

reportWebVitals();

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
import { UserContext } from '../utils/context';

function Login() { 
  // use userLogin function from UserContext
  const { userLogin } = useContext(UserContext);
  // this data is loaded form server side, now just an example
  const user = {
    username: 'logged username', id: 'id', token: 'xyzqwtr..'}
  }
  // send user data to UserContext
  userLogin(user);
}

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
import { UserContext } from '../context';

function Some() {
  // read user data from UserContext
  const { user } = useContext(UserContext);

  useEffect(() => {
    // check if user is logged in or not
    if (!user.auth) {
      // user is not logged in -> do something!
    } else {
      // user is logged in!
    }
  });

}

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.

Read more