Skip to main content

State Management

State management refers to the process of managing the state of an application efficiently. In React, the "state" represents the dynamic data of a component or application, which can change over time as users interact with the UI.

State management ensures:

  1. Data consistency across components.
  2. Efficient updates to the UI based on state changes.
  3. Simplified communication between components.

State Management with useState​

The useState hook is the most basic way to manage state in a React functional component.

Characteristics​

  • Manages local state for a single component.
  • Best for simple, isolated state needs.

Syntax​

const [state, setState] = useState(initialState);
  • state: The current state value.
  • setState: A function to update the state.

Example​

import React, { useState } from "react";

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

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}

When to Use​

  • For simple state logic.
  • For component-specific state.

State Management with useReducer​

The useReducer hook is a more powerful alternative to useState for managing complex state logic.

Characteristics​

  • Suitable for complex state transitions.
  • Uses a reducer function to determine how state updates based on actions.

Syntax​

const [state, dispatch] = useReducer(reducer, initialState);
  • reducer: A function (state, action) => newState that defines state transitions.
  • dispatch: A function to trigger state changes.

Example​

import React, { useReducer } from "react";

function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
throw new Error("Unknown action type");
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });

return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>Increment</button>
<button onClick={() => dispatch({ type: "decrement" })}>Decrement</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}

When to Use​

  • For complex state logic involving multiple actions.
  • When the next state depends on the previous state.

State Management with Context API​

The Context API provides a way to share state across multiple components without passing props down manually at every level.

Characteristics​

  • Solves the prop drilling problem (passing props through many levels).
  • Not suitable for frequent state updates (due to performance overhead).

Key Components​

  1. Context Provider:

    • Wraps components and provides the shared state.
    const MyContext = React.createContext();
  2. Context Consumer:

    • Consumes the shared state.

Example​

import React, { useState, useContext } from "react";

// Create context
const ThemeContext = React.createContext();

function App() {
const [theme, setTheme] = useState("light");

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}

function Toolbar() {
const { theme, setTheme } = useContext(ThemeContext);

return (
<div>
<p>Current Theme: {theme}</p>
<button onClick={() => setTheme("dark")}>Switch to Dark Theme</button>
</div>
);
}

When to Use​

  • To share state globally across the component tree.
  • For static or infrequently changing state (e.g., theme, user authentication).

State Management with Redux​

Redux is a predictable state management library often used with React for managing application-level state.

Characteristics​

  • Centralizes state in a single store.
  • State is updated using pure reducer functions.
  • Provides predictable state transitions.

Core Concepts​

  1. Store: The centralized state container.
  2. Actions: Objects describing what to do (e.g., { type: 'INCREMENT' }).
  3. Reducer: A function (state, action) => newState that handles state transitions.

Example​

import { createStore } from "redux";

// Reducer
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}

// Create Store
const store = createStore(counterReducer);

// Dispatch Actions
store.dispatch({ type: "INCREMENT" });
console.log(store.getState()); // { count: 1 }

When to Use​

  • For large applications where state needs to be shared across many components.
  • When state transitions need to be predictable and easy to debug.

Redux Middleware (Thunk and Saga)​

Middleware in Redux extends the store's capabilities by handling side effects like asynchronous actions.

Redux Thunk​

  • A middleware for handling async actions in Redux.
  • Allows action creators to return functions (instead of objects).

Example:

// Action Creator with Thunk
function fetchData() {
return async (dispatch) => {
const response = await fetch("/api/data");
const data = await response.json();
dispatch({ type: "SET_DATA", payload: data });
};
}

Redux Saga​

  • A middleware for handling complex side effects using generator functions.
  • Provides better control over side effects compared to Thunk.

Example:

import { call, put, takeEvery } from "redux-saga/effects";

function* fetchDataSaga() {
const data = yield call(fetch, "/api/data");
yield put({ type: "SET_DATA", payload: data });
}

function* watchFetchData() {
yield takeEvery("FETCH_DATA", fetchDataSaga);
}

When to Use​

  • Use Thunk for simple async logic (e.g., API calls).
  • Use Saga for complex async workflows (e.g., orchestrating multiple side effects).

State Management with Redux Toolkit​

Redux Toolkit (RTK) simplifies Redux by providing utilities for setting up a Redux store with less boilerplate.

Features​

  1. configureStore: Sets up the store with middleware pre-configured.
  2. createSlice: Automatically generates actions and reducers.
  3. createAsyncThunk: Simplifies async actions.

Example​

import { configureStore, createSlice } from "@reduxjs/toolkit";

// Slice
const counterSlice = createSlice({
name: "counter",
initialState: { count: 0 },
reducers: {
increment: (state) => {
state.count += 1;
},
decrement: (state) => {
state.count -= 1;
},
},
});

const store = configureStore({
reducer: { counter: counterSlice.reducer },
});

// Dispatch Actions
store.dispatch(counterSlice.actions.increment());
console.log(store.getState()); // { counter: { count: 1 } }

When to Use​

  • For simplified Redux setup.
  • When you want to avoid the boilerplate of traditional Redux.

Summary Table​

MethodUse CaseComplexity
useStateLocal state, simple componentsLow
useReducerComplex state logic, multiple actionsMedium
Context APISharing global state across the component treeMedium
ReduxLarge-scale apps with shared, predictable stateHigh
Redux ThunkAsync actions in ReduxMedium
Redux SagaComplex async workflowsHigh
Redux ToolkitSimplified Redux setup, reduced boilerplateMedium