React State Management: A Guide 2023
React State Management

React State Management: A Guide 2023

Dead Simple Chat Team

In this article we are going to learn state management in React. This is an overview of the things that we are going to learn

If you are looking for a React Native Chat SDK to build in app chat messaging application, you can consider DeadSimpleChat React Native SDK

  • Why state management is important in React
  • When developing complex and large scale applications what are the state management issues faced by developers
  • What are the built in state management solutions provided by React
  • Popular State Management Libraries available today, including Redux, MobX, and Recoil
  • How to implement advanced state management patterns and best practices
  • Selecting specific state management techniques according to project requirement

Importance of state management in react

  • How react applications handle data
  • Importance of having consistancy when handling data
  • How to imporve maintainability to react applications
  • Utilizing best performance from react applications

State management issues when developing Complex and large scale applications

In this section we are going to look at some of the issues faced by developers when working on complex and large scale applications

Prop Drilling: Passing data form Parent component to child component through props becomes tiresome and difficult as the application grows.

In large heirarchial components passing props from parents to deeply nested child component through props is known as prop drilling.

You can use state management technologies like useContext and useRef or external state management libraries to mitigate this problem

Optimizing and Maintaing Performance as application Grows: Maintaining the consistance of state across multiple components becomes challenging when developing large scale applications

In this article we will explore technique on how to do this. Some of the methods are Optimizing state updates, avoiding unnessesory re renders and performance improvements with tools like React Devtools

Asynchronous state updates : Asynchronous state upadtes and side effects can sometimes introduces complexity and race conditions

With the help of state management libraries like Redux and middlewares like Sagas as well as Recoild Sync selectors we can mitigate the risks

Built In State Management tools: useState and useReducer

In this section we will be discussing the built in state management technologies in react name the useState and useReducer hooks

useState

It is a core hook in react, useState is used to manage state, with useState you get the state variable and the setState function that you can use update the state of a component

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Current count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Add</button>
    </div>
  );
};
useState in react

Here we are initializing the state count with the useState and setting the initial value to 0

then we are using the setCount funtion to update the state by 1 whenever the Add button is clicked

useReducer

usereducer is a state management hook that is used along with  a reducer function and an initial state

this function will work well with state Objects having multiple properties and complex state structures

This is how the reducer function works

  • The reducer function takes the current state and a dispatch function for invoking actions
  • So, to reiterate It takes the current state and an action and return the updated state
  • This pattern is similar to the recducers in Redux through it works locally inside a component

let us consider an example

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return { count: state.count + 1 };
    default:
      throw new Error('this is not allowed');
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p> This point in time count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'add' })}>Add</button>
    </div>
  );
};
useReducer

In the above example, we have the initial state and a reducer function. We are providing both to the useReducer hook. the reducer function here handles the state

Both useState and useReducer are state management tools in react

In this section we are going to consider what are some of the popular state management patterns and what are their strengrth and weaknesses

Lifting the state up

many times you want to share state between components, a possible solution to this is moving the state to a common parent component.

To enable the sibling components to share the state, a callback function provided by the parent.

This methodology is called as lifting the state up.

Strengths of the method: Lifting the state up

Built in pattern: Lifting the state up is a built in pattern in react and this is encouraged by react. thus does not require any external dependencies or libraries to function

Easy to use : By consolidating the shared state in a common parent and enabling the siblings to use the state

Sibling components can communicate : It provides a good way for the siblings to talk with one another without using advanced state management techniques

Weakness:

Prop Drilling: We studies prop drilling previously in this article. basically in component that have a large hierarchy it becomes difficult to pass data to child component nested deep inside the component tree

Scalability issues: This methodology cannot be scaled to large components because of issues like prop drilling and advanced state management techniques are required.

Local Component state

Encapsulation: With local component state, every component has its own state that is not shared with other components. This leads to a sence of encapsulation

Where each component stores the data that is relevant to it inside the component

Flexibility : With react hooks like useState and useReducer you can easily manage state and this provides flexibility both for simple as well as complex state management requirements

Performance: This methods avoids isssues like unnessessory re renders and other performance issues and keeps state management simple

In this section we will briefly touch up the popular state management libraries and thier core concepts

Then in the next section we will dig deeper into these libraries

Redux:

Redux is a well know state management library based on the principles of single immutable state tree, actions, and reducers

Core Features

  • State tree: A single Javascript Object called the store has the state for the entire application
  • Actions: Actions are dispatched to trigger state changes. They contain a payload with information describing state changes
  • Reducers: reducers are javascript functions. They handle state changes based on the information payload provided by the dispatched actions. When given the current state and an action they return the updated state

Use-Case: Redux is beneficial in large scale applications, where unidirectional flow on information and predictable state container are important so as not to increase complexcity and avoid confusion

One trade off is that there is an advanced learning curve when learning about redux and it is difficult and too much effort for smaller applicaitons

MobX

MobX is a reactive state management library, that has core concepts like

Observables, computed values and actions to automatically manage state updates

Core Features

Observables: Using MobX you can annotate state variables as Obsevables thus making them reactive to change

Computed Values: Computed values update automatically when the dependencies change and are derived from the state values

Actions: To manage side effects actions can be used to modify observable state explicitly

use cases

MobX is best if you like reactive applications where the state automatically updates and it automatically minimizes boilerplate code and are easy to work with

Trade-Off

MobX is not suited for very large applications and can create confusion when debugging and can obfuscate understanding of the underlying logic

It also relies on decorators which is a fairly new concept in javascript

Recoil

Recoil is a newer state management library and it too like react is developed by facebook.

Core Features

Atoms: Recoil has a concept of atoms. Atoms are basically units of self contained state and they can be read and updated directly by the component

Selectors: Selector generate derived state from atoms and other selectors. These can be asynchronous and can handle side effects easily

Use cases

Recoil has complex derived state and asynchronous state updates, It has selectors which work quite well with react suspense and concurrent mode

Trade offs

Recoil's ecosystem is new and less mature than Redux or MobX but never the less it is a good library and hopefully with time there will be adequte support for Recoil as well

Redux: how to with examples

Redux is known for unidirectional flow of data and predictability with regards to state management

This leads to easier state management in large scale applications

Core Concepts

Actions: Actions are plain JavaScript Objects containing 1. Payload with data and type property which indicates which type of action is to be performed

// An example of an action in Redux
{
  type: 'ADD_COUNT',
  payload: 1,
}

Reducers:

Reducers are pure functions that can reciever the current state and an action, they return the new state

The reducer specifies how the state will update in response to an action

const initialState = { count: 0 };

// An example of a reducer in redux
function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'ADD_COUNT':
      return { ...state, count: state.count + action.payload };
    default:
      return state;
  }
}

Store: Store holds the complete state of the application in redux. all the state is stored in one place which is called the store

You can create the store using the createStore function and a reducer that manages the state updates

import { createStore } from 'redux';
import counterReducer from './counterReducer';

const store = createStore(counterReducer);

Example

First create a new react application with npx

npx create-next-app

then install the redux package

npm install redux react-redux --save

In this section we will be creating a counter application with Redux

  1. Create the action types, action creators and the reducer function
// actionTypes.js
export const INCREMENT_COUNT = 'INCREMENT_COUNT';

// actions.js
import { INCREMENT_COUNT } from './actionTypes';

export const incrementCount = (amount) => ({
  type: INCREMENT_COUNT,
  payload: amount,
});

// reducers/counterReducer.js
import { INCREMENT_COUNT } from '../actionTypes';

const initialState = { count: 0 };

export default function counterReducer(state = initialState, action) {
  switch (action.type) {
    case INCREMENT_COUNT:
      return { ...state, count: state.count + action.payload };
    default:
      return state;
  }
}

2. Then work on creating the Redux store and connect it to react like so. this way

// store.js
import { createStore } from 'redux';
import counterReducer from './reducers/counterReducer';

const store = createStore(counterReducer);

export default store;

// index.js
import { Provider } from 'react-redux';
import store from './store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

3. Creating the counter component and connecting it to the redux store like

// Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { incrementCount } from './actions';

const Counter = () => {
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(incrementCount(1))}>Increment</button>
    </div>
  );
};

export default Counter;

Middleware Performance Optimization and Redux DevTools

Middleware: There are these middleware functions. These functions can intercept dispatched actions before they reach the reducer function

You can perform various tasks with the middleware functions like

  • logging in
  • handling side effects
  • modifiying the action

The popular middleware for Redux includes Redux Thunk and Redux Saga these can be used to handle asynchronous actions

// middleware example with redux
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

const store = createStore(rootReducer, applyMiddleware(thunk));

Performance Optimization

There are various ways to optimize the performance in Redux. these include

  • useSelector : does a shallow comparison to avoid unnessesory rerenders
  • React.memo() : Memorizes the functional components
  • reselect : library can be used to created memorized selectors for derived state computations

Redux DevTools

It is a browser extention that provides debugging capabilities, you can inspect dispatched actions, visualize application state and time travel in state history

You can use it like this

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

Mobx In Detail

MobX simplifies state management in react applications. It has concepts of Observables, computed values and actions.

Key concepts

Observables: Observables are state variables that are reactive. When Observables change the MobX re renders the component

Computed Values: These values are computed and are based on their dependencies. When dependencies change the computed values are rendered again. These values are sometimes cached to optimize the performance

Actions: Actions are plain javascript functions that can change Observable state values. These functions make state changes more predictable and can be used to handle side effects as well

Practical Example

install the MobX in your react application

npm install mobx mobx-react-lite --save

In this example we are going to create a todo application

  1. In the first step we need to set up a MobX store that will store our state
// todoStore.js
import { makeAutoObservable } from 'mobx';

class TodoStore {
  todos = [];

  get incompleteTodos() {
    return this.todos.filter((todo) => !todo.completed);
  }

  constructor() {
    makeAutoObservable(this);
  }

  addTodo(text) {
    this.todos.push({ text, completed: false });
  }

  toggleTodo(index) {
    this.todos[index].completed = !this.todos[index].completed;
  }
}

const todoStore = new TodoStore();

export default todoStore;

Then we need to create a TodoList Component and we connect this component to the MobX store that we just created

// TodoList.js
import React, { useState } from 'react';
import { observer } from 'mobx-react-lite';
import todoStore from './todoStore';

const TodoList = observer(() => {
  const [text, setText] = useState('');

  const handleAddTodo = () => {
    todoStore.addTodo(text);
    setText('');
  };

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={handleAddTodo}>Add Todo</button>
      <ul>
        {todoStore.todos.map((todo, index) => (
          <li key={index}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => todoStore.toggleTodo(index)}
            />
            {todo.text}
          </li>
        ))}
      </ul>
      <p>Incomplete todos: {todoStore.incompleteTodos.length}</p>
    </div>
  );
});

export default TodoList;

And that it we have created a react application with state management done by MobX

Recoil in detail

Recoil is built by facebook which also built react. Recoil has introduced a number of concepts to handle complex and derived state management in react.

Recoil also provides performance improvements and aims to be minimal and flexible solution balancing state consistancy, performance and maintainability

Core Concepts: Atoms and Selectors

Atoms are independent units of state in recoil, these hold primitive values and hence can be thought of as basic building blocks of state in react

Individual atoms can be subscribed by components and when an atom updates the components that are subscribed to that atom are updated and this improves performance

Let us learn more with the help of an example. In this example we are going to be creating a user auth with Recoil

import { atom } from 'recoil';

const isAuthenticatedAtom = atom({
  key: 'isAuthenticated',
  default: false,
});

Selectors: Selectors are pure functions. They provide an efficient mechanism for managing complex and derived state.

These selectors derive their state from atoms or other selectors.

Recoil will automatically update selectors when their dependencies change and memorize their outputs for performance improvements

Example of a selector that derives a value from the auth state

import { selector } from 'recoil';
import { isAuthenticatedAtom } from './isAuthenticatedAtom';

const currentUserSelector = selector({
  key: 'currentUser',
  get: ({ get }) => {
    const isAuthenticated = get(isAuthenticatedAtom);
    
    if (isAuthenticated) {
      return { name: 'John Doe', email: 'john@company.com' };
    }
    
    return null;
  }
});

Recoil in complex react applications

there are custom recoil hooks available to manage state in complex and large scale applications with Recoil

  • useRecoilState
  • useRecoilValue
  • useSetRecoilState

these hooks let you read and write data to your state

Let us look at an application where we will create an auth app with recoil

import React from 'react';
import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
import { isAuthenticatedAtom, currentUserSelector } from './state';

function App() {
  return (
    <RecoilRoot>
      <UserAuthentication />
    </RecoilRoot>
  );
}

function UserAuthentication() {
  const [isAuthenticated, setIsAuthenticated] = useRecoilState(isAuthenticatedAtom);
  const currentUser = useRecoilValue(currentUserSelector);

  const handleButtonClick = () => {
    setIsAuthenticated(prevState => !prevState);
  };

  return (
    <div>
      <button onClick={handleButtonClick}>
        {isAuthenticated ? 'Log Out' : 'Log In'}
      </button>
      {currentUser && <p>Welcome, {currentUser.name} ({currentUser.email})</p>}
    </div>
  );
}

export default App;

Async Selectors

Recoil also has support for async selectors that let you handle async actions like fetching data from an external APIs.

Async selectors can be used with react Suspense to fetch data and display it in your application and for a easy user experience

Here is an example os using async selectors

import { selector } from 'recoil';

const fetchUser = async (userId) => {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  return response.json();
};

const userSelector = selector({
  key: 'user',
  get: async ({ get }) => {
    const userId = get(userAtom);
    return userId ? await fetchUser(userId) : null;
  }
});

DeadSimpleChat

Need Chat API for your website or app

DeadSimpleChat is an Chat API provider

  • Add Scalable Chat to your app in minutes
  • 10 Million Online Concurrent users
  • 99.999% Uptime
  • Moderation features
  • 1-1 Chat
  • Group Chat
  • Fully Customizable
  • Chat API and SDK
  • Pre-Built Chat

Conclusion

In this article we learned about state management in react and what are some of the built in state management tools and what are some of the external libraries available for state management

We also learned how to use these libraries with examples and what are the use cases that fit best for each of these also where using the built in tools is the best solution

I hope you liked the article. Thank you for reading