What is State Management in React – Redux, Recoil and Zustand

State management is an important aspect of creating robust and scalable React applications. Traditional methods such as prop drilling can work for small apps but larger apps call for more advanced state management solutions. This blog post will discuss about state management in React, how it is done, the reasons behind and different popular libraries/techniques used to manage effective state.

1. Introduction to State Management in React


In a predictable way, efficiently handle component’s or all application’s states are called “state management” within a React application. Component holds information that may change over time and affect its rendering.

According to the documentation on react.dev, it is similar to props but private and completely controlled by this component only.

1.1 Why Do We Need State Management in React?

  • Data consistency across components: This ensures data consistency across the application state.
  • Scalability: Managing state becomes easier when you are dealing with large applications.
  • Ease of Maintenance: It makes debugging, testing and maintenance easier since they all have clear structures for changing states.

A recent State of JS 2022 survey shows that managing application state is one of the most critical aspects of React development, especially in complex and large-scale applications.

2. How does state management work with React?


The built-in hooks like useState and useReducer, which are available in React, can be used for local state management while for global state management the developers usually employ the Context API or third-party libraries such as Redux, Recoil, Zustand, and Jotai.

2.1 Local State Management


This means that it manages state within individual components. It is suitable for small applications or components that don’t have to share state with other ones.

Imagine you’re building a simple Todo List app.

import React, { useState } from 'react';  
  
const TodoApp = () => {  
  const [todos, setTodos] = useState([]);  
  const [todo, setTodo] = useState('');  
  
  const addTodo = () => {  
    setTodos([...todos, { id: todos.length + 1, text: todo }]);  
    setTodo('');  
  };  
  
  return (  
    <div>  
      <input value={todo} onChange={(e) => setTodo(e.target.value)} placeholder="Add a new todo" />  
      <button onClick={addTodo}>Add Todo</button>  
      <ul>  
        {todos.map((item) => (  
          <li key={item.id}>{item.text}</li>  
        ))}  
      </ul>  
    </div>  
  );  
};  
  
export default TodoApp;



An example is given here to show how state can be managed locally within a single component using useState. Suitable for small applications where in-component state changes take place.

However if you want more complex state management, you may want to consider using useReducer

import React, { useReducer, useState } from 'react';  
  
const initialState = { todos: [] };  
  
function reducer(state, action) {  
  switch (action.type) {  
    case 'ADD_TODO':  
      return { todos: [...state.todos, action.payload] };  
    default:  
      return state;  
  }  
}  
  
const TodoApp = () => {  
  const [state, dispatch] = useReducer(reducer, initialState);  
  const [todo, setTodo] = useState('');  
  
  const addTodo = () => {  
    dispatch({ type: 'ADD_TODO', payload: { id: state.todos.length + 1, text: todo } });  
    setTodo('');  
  };  
  
  return (  
    <div>  
      <input value={todo} onChange={(e) => setTodo(e.target.value)} placeholder="Add a new todo" />  
      <button onClick={addTodo}>Add Todo</button>  
      <ul>  
        {state.todos.map((item) => (  
          <li key={item.id}>{item.text}</li>  
        ))}  
      </ul>  
    </div>  
  );  
};  
  
export default TodoApp;



This example illustrates a more complex case of state management when working with useReducer. It is recommended for applications that have a complex state transition and need to control it predictably.

2.2 Global State Management


On the other hand, global state management is referred as managing states which can be accessed by multiple components across the application and need updates. Unlike local states which only exist within a single component, global states are distributed throughout an application so that they could be shared and synchronized between different parts of it.

Why use State Management in React?


Benefits of using state management solutions include

  • Prop Drilling Avoidance: Eliminating passing props through multiple levels of components.
  • Improved Code Organization: This separates out the code that handles states and makes them modularized.
  • Enhancing Performance: Efficient state management that optimizes re-renders and updates

Kent C. Dodds in a blog post says that for React to be effective in its performance and maintainability, it should have a good state management.

3. React State Management Solutions


Let’s take the Todo List application scenario where we will go through each of them.

3.1 Context API


React provides a built-in feature called Context API which allows you to share state across components without passing props down the component tree.

The official React documentation describes Context API as a way of passing data throughout the component tree without having to pass props manually at every level.

3.1.1 When to Use Context API?

  • Global State: It is ideal for managing global states such as themes, user authentication, settings among others.
  • Simple State Needs: Applications with simple state management requirements can effectively use this mechanism of managing it.

import React, { createContext, useState, useContext } from 'react';  
  
// Create a context  
const TodoContext = createContext();  
  
// Create a provider component  
const TodoProvider = ({ children }) => {  
  const [todos, setTodos] = useState([]);  
  const addTodo = (todo) => setTodos([...todos, todo]);  
  
  return (  
    <TodoContext.Provider value={{ todos, addTodo }}>  
      {children}  
    </TodoContext.Provider>  
  );  
};  
  
// Custom hook for using the context  
const useTodos = () => useContext(TodoContext);  
  
// Example component  
const TodoApp = () => {  
  const { todos, addTodo } = useTodos();  
  const [todo, setTodo] = useState('');  
  
  const handleAddTodo = () => {  
    addTodo({ id: todos.length + 1, text: todo });  
    setTodo('');  
  };  
  
  return (  
    <div>  
      <input value={todo} onChange={(e) => setTodo(e.target.value)} placeholder="Add a new todo" />  
      <button onClick={handleAddTodo}>Add Todo</button>  
      <ul>  
        {todos.map((item) => (  
          <li key={item.id}>{item.text}</li>  
        ))}  
      </ul>  
    </div>  
  );  
};  
  
// App component  
const App = () => (  
  <TodoProvider>  
    <TodoApp />  
  </TodoProvider>  
);  
  
export default App;



An instance explaining how context API can be used to handle global state. This simplifies the process of passing down states across multiple components without drilling props.

4. Redux

Redux is a popular JavaScript library used for managing an application’s state in centralized store. In Redux data flows unidirectionally, and its states are updated by using actions along with reducers.

According to Redux’s official documentation, this library helps you write applications that behave consistently running in different environments (client, server, and native) as well as easily tested ones.

4.1 When to Use Redux?

  • Complex State Needs: Applications that require complex state management.
  • Predictable State: When you need a predictable flask for JavaScript applications.

import { createStore } from 'redux';  
import { Provider, useDispatch, useSelector } from 'react-redux';  
  
// Define initial state  
const initialState = {  
  todos: [],  
};  
  
// Define reducer  
const todoReducer = (state = initialState, action) => {  
  switch (action.type) {  
    case 'ADD_TODO':  
      return { ...state, todos: [...state.todos, action.payload] };  
    default:  
      return state;  
  }  
};  
  
// Create store  
const store = createStore(todoReducer);  
  
// Example component  
const TodoApp = () => {  
  const dispatch = useDispatch();  
  const todos = useSelector((state) => state.todos);  
  const [todo, setTodo] = useState('');  
  
  const handleAddTodo = () => {  
    dispatch({ type: 'ADD_TODO', payload: { id: todos.length + 1, text: todo } });  
    setTodo('');  
  };  
  
  return (  
    <div>  
      <input value={todo} onChange={(e) => setTodo(e.target.value)} placeholder="Add a new todo" />  
      <button onClick={handleAddTodo}>Add Todo</button>  
      <ul>  
        {todos.map((item) => (  
          <li key={item.id}>{item.text}</li>  
        ))}  
      </ul>  
    </div>  
  );  
};  
  
// App component  
const App = () => (  
  <Provider store={store}>  
    <TodoApp />  
  </Provider>  
);  
  
export default App;



In this example, Redux is utilized to implement React application state management. Redux is mostly helpful in apps where there are high demands due to complexity of the application’s state management requirements and therefore avails a predictable container for states.

5. Recoil


A state management library developed by Facebook called Recoil which offers more flexible and scalable solutions than Context API.

According to the Recoil documentation, it has a similar working process as React. It allows developers to manage the state of large applications with minimum boilerplate code.

5.1 When to Use Recoil?

  • Complex State Needs: Recommended for applications with more complicated state management requirements.
  • Scalability: For big applications only.

import React from 'react';  
import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue } from 'recoil';  
  
// Create an atom  
const todoListState = atom({  
  key: 'todoListState',  
  default: [],  
});  
  
// Create a selector  
const todoListLengthState = selector({  
  key: 'todoListLengthState',  
  get: ({ get }) => {  
    const todoList = get(todoListState);  
    return todoList.length;  
  },  
});  
  
// Example component  
const TodoApp = () => {  
  const [todoList, setTodoList] = useRecoilState(todoListState);  
  const todoListLength = useRecoilValue(todoListLengthState);  
  const [todo, setTodo] = useState('');  
  
  const addTodo = () => {  
    setTodoList([...todoList, { id: todoListLength + 1, text: todo }]);  
    setTodo('');  
  };  
  
  return (  
    <div>  
      <input value={todo} onChange={(e) => setTodo(e.target.value)} placeholder="Add a new todo" />  
      <button onClick={addTodo}>Add Todo</button>  
      <ul>  
        {todoList.map((item) => (  
          <li key={item.id}>{item.text}</li>  
        ))}  
      </ul>  
    </div>  
  );  
};  
  
// App component  
const App = () => (  
  <RecoilRoot>  
    <TodoApp />  
  </RecoilRoot>  
);  
  
export default App;



For instance, this showcases how Recoil can be deployed as an alternative form of state management. It provides minimal-boilerplate scalable-state-management solutions for app development.


6. Zustand


Zustand is a small, fast state management library that uses hooks for managing state.

The Zustand GitHub repository describes it as a small, fast, and scalable state management solution for React.


6.1 When Zustand is to be Used?

  • Simple to Medium Complexity: Perfect for applications with state management needs ranging from simple to medium complexity.
  • Performance: Best for applications that require high performance.

import create from 'zustand';  
import React from 'react';  
  
// Create a store  
const useStore = create((set) => ({  
  todos: [],  
  addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),  
}));  
  
// Example component  
const TodoApp = () => {  
  const { todos, addTodo } = useStore();  
  const [todo, setTodo] = useState('');  
  
  const handleAddTodo = () => {  
    addTodo({  
      id: todos.length + 1,  
      text: todo,  
    });  
    setTodo('');  
  };  
  
  return (  
    <div>  
      <input value={todo} onChange={(e) => setTodo(e.target.value)} placeholder="Add a new todo" />  
      <button onClick={handleAddTodo}>Add Todo</button>  
      <ul>  
        {todos.map((item) => (  
          <li key={item.id}>{item.text}</li>  
        ))}  
      </ul>  
    </div>  
  );  
};  
  
// App component  
const App = () => (  
  <div>  
    <TodoApp />  
  </div>  
);  
  
export default App;



A case study on how Zustand may be employed in managing states on the react side of things. Performance driven and simple – moderately complex cases of state handling in an application would find appropriate use cases.

7. State Management in React Native


What is State Management in React Native?

React Native’s management of the state functions like its counterpart on web apps. These principles and libraries (Context API, Redux, Recoil, Zustand, Jotai) can also be used for managing state in React native.

According to the React Native documentation, this way of handling state in React Native is basically identical with how it would be done in a typical react web application hence allowing the same coding practices across platforms.

7.1 Why Use State Management in React Native?

  • Consistency Across Platforms: Ensures uniformity when managing states on different platforms (iOS, Android).
  • Shared State: Sharing state and logic between web and mobile apps.

Let us say you are creating a cross-platform application for keeping to-do list using react native and Zustand for state management.

import create from 'zustand';  
import { Text, TextInput, Button, View, FlatList } from 'react-native';  
  
// Create a store  
const useStore = create((set) => ({  
  todos: [],  
  addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),  
}));  
  
// Example component  
const TodoApp = () => {  
  const [text, setText] = React.useState('');  
  const { todos, addTodo } = useStore();  
  
  return (  
    <View>  
      <TextInput  
        value={text}  
        onChangeText={setText}  
        placeholder="Add a new todo"  
      />  
      <Button  
        title="Add Todo"  
        onPress={() => {  
          addTodo({  
            id: todos.length + 1,  
            text,  
          });  
          setText('');  
        }}  
      />  
      <FlatList  
        data={todos}  
        keyExtractor={(item) => item.id.toString()}  
        renderItem={({ item }) => <Text>{item.text}</Text>}  
      />  
    </View>  
  );  
};  
  
export default TodoApp;



To illustrate this, Zustand is demonstrated as the library that manages the status of an app written in React Native framework. This emphasizes uniformity in handling states regardless of platform differences.

8. Choosing the Best State Management Solution

The best solution to managing state depends on your specific use case or application requirements.

  • Context API: It works well for basic simple states as well as global states such as themes and user settings.
  • Redux: Great for complicated state control and applications which need a predictable state holder.
  • Recoil: Good for developing scalable apps using complex states.
  • Zustand: Suitable for performance intensive apps that are simple to moderately complex.

As stated in State of JS 2022 survey, Redux is still popular, but Recoil and Zustand are also becoming popular as they offer simplicity and better performance.

8.1 When should you use React State management?

  • Complex State Logic: When you have complex state logic that needs to be managed predictably within your app.
  • Global State Sharing: Whenever you want to pass certain values from a parent component down through many levels of child components without explicitly passing them as props at each level.
  • Performance Optimization: When optimizing performance by re-rendering minimization and efficient state management.

Conclusion

Scalable and maintainable React applications would not be possible without proper state management. Although prop drilling is good enough for some basic use cases, more advanced ones require powerful tools like Context API, Redux, Recoil, Zustand. You can choose the best state management solution for your React projects by knowing their strengths and use-cases.

Additionally, try to be mindful of memory leaks and implement best practices to keep your application efficient.

Happy Coding!

Leave a Reply

Your email address will not be published. Required fields are marked *

×