React State Management Interview Questions

Table of Contents +

State management questions often start with the big-picture architectures, then move into Redux fundamentals, async flows, tooling, immutable updates, and finally modern hooks-based state patterns. This page now follows that broader progression so the topic feels more like a guided interview path than a flat reference dump.

This page includes 61 questions in this topic group.

State management foundations

What is flux?

Flux is an application architecture (not a framework or library) designed by Facebook to manage data flow in React applications. It was created as an alternative to the traditional MVC (Model-View-Controller) pattern, and it emphasizes a unidirectional data flow to make state changes more predictable and easier to debug.

Flux complements React by organizing the way data moves through your application, especially in large-scale or complex projects.

Core Concepts of Flux

Flux operates using four key components, each with a specific responsibility:

  • Actions
    • Plain JavaScript objects or functions that describe what happened (e.g., user interactions or API responses).
    • Example: { type: 'ADD_TODO', payload: 'Buy milk' }
  • Dispatcher
    • A central hub that receives actions and dispatches them to the appropriate stores.
    • There is only one dispatcher in a Flux application.
  • Stores
    • Hold the application state and business logic.
    • Respond to actions from the dispatcher and update themselves accordingly.
    • They emit change events that views can listen to.
  • Views (React Components)
    • Subscribe to stores and re-render when the data changes.
    • They can also trigger new actions (e.g., on user input).

The workflow between dispatcher, stores and views components with distinct inputs and outputs as follows:

What is Redux?

Redux is a predictable state container for JavaScript applications, most commonly used with React. It helps you manage and centralize your application’s state in a single source of truth, enabling easier debugging, testing, and maintenance-especially in large or complex applications. Redux core is tiny library(about 2.5kB gzipped) and has no dependencies.

What are the core principles of Redux?

Redux follows three fundamental principles:

  1. Single source of truth: The state of your whole application is stored in an object tree within a single store. The single state tree makes it easier to keep track of changes over time and debug or inspect the application.
const store = createStore(reducer);
  1. State is read-only: The only way to change the state is to emit an action, an object describing what happened. This ensures that neither the views nor the network callbacks will ever write directly to the state.
const action = { type: 'INCREMENT' };
store.dispatch(action);
  1. Changes are made with pure functions(Reducers): To specify how the state tree is transformed by actions, you write reducers. Reducers are just pure functions that take the previous state and an action as parameters, and return the next state.
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}

What are the downsides of Redux compared to Flux?

While Redux offers a powerful and predictable state management solution, it comes with a few trade-offs when compared to Flux. These include:

  1. Immutability is essential
    Redux enforces a strict immutability model for state updates, which differs from Flux’s more relaxed approach. This means you must avoid mutating state directly. Many Redux-related libraries assume immutability, so your team must be disciplined in writing pure update logic. You can use tools like redux-immutable-state-invariant, Immer, or Immutable.js to help enforce this practice, especially during development.
  2. Careful selection of complementary packages
    Redux is more minimal by design and provides extension points such as middleware and store enhancers. This has led to a large ecosystem, but it also means you must thoughtfully choose and configure additional packages for features like undo/redo, persistence, or form handling-something Flux explicitly leaves out but may be simpler to manage in smaller setups.
  3. Limited static type integration
    While Flux has mature support for static type checking with tools like Flow, Redux’s type integration is less seamless. Although TypeScript is commonly used with Redux now, early Flow support was limited, and more boilerplate was required for static type safety. This may affect teams that rely heavily on type systems for large codebases.

Core Redux usage patterns

What is the difference between mapStateToProps() and mapDispatchToProps()?

mapStateToProps() is a utility which helps your component get updated state (which is updated by some other components):

const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter),
};
};

mapDispatchToProps() is a utility which will help your component to fire an action event (dispatching action which may cause change of application state):

const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id));
},
};
};

It is recommended to always use the β€œobject shorthand” form for the mapDispatchToProps.

Redux wraps it in another function that looks like (…args) => dispatch(onTodoClick(…args)), and pass that wrapper function as a prop to your component.

const mapDispatchToProps = {
onTodoClick,
};

Can you dispatch an action in reducer?

Dispatching an action within a reducer is an anti-pattern. Your reducer should be without side effects, simply digesting the action payload and returning a new state object. Adding listeners and dispatching actions within the reducer can lead to chained actions and other side effects.

How do you access Redux store outside a component?

You just need to export the store from the module where it created with createStore(). Also, it shouldn’t pollute the global window object.

store = createStore(myReducer);
export default store;

What are the drawbacks of MVW pattern?

  1. DOM manipulation is very expensive which causes applications to behave slow and inefficient. 2. Due to circular dependencies, a complicated model was created around models and views. 3. Lot of data changes happens for collaborative applications(like Google Docs). 4. No way to do undo (travel back in time) easily without adding so much extra code.

Are there any similarities between Redux and RxJS?

These libraries are very different for very different purposes, but there are some vague similarities.

Redux is a tool for managing state throughout the application. It is usually used as an architecture for UIs. Think of it as an alternative to (half of) Angular. RxJS is a reactive programming library. It is usually used as a tool to accomplish asynchronous tasks in JavaScript. Think of it as an alternative to Promises. Redux uses the Reactive paradigm because the Store is reactive. The Store observes actions from a distance, and changes itself. RxJS also uses the Reactive paradigm, but instead of being an architecture, it gives you basic building blocks, Observables, to accomplish this pattern.

How do you reset state in Redux?

You need to write a root reducer in your application which delegate handling the action to the reducer generated by combineReducers().

For example, let’s take rootReducer() to return the initial state after USER_LOGOUT action. As we know, reducers are supposed to return the initial state when they are called with undefined as the first argument, no matter the action.

const appReducer = combineReducers({
/* your app's top-level reducers */
});
const rootReducer = (state, action) => {
if (action.type === "USER_LOGOUT") {
state = undefined;
}
return appReducer(state, action);
};

In case of using redux-persist, you may also need to clean your storage. redux-persist keeps a copy of your state in a storage engine. First, you need to import the appropriate storage engine and then, to parse the state before setting it to undefined and clean each storage state key.

const appReducer = combineReducers({
/* your app's top-level reducers */
});
const rootReducer = (state, action) => {
if (action.type === "USER_LOGOUT") {
Object.keys(state).forEach((key) => {
storage.removeItem(`persist:${key}`);
});
state = undefined;
}
return appReducer(state, action);
};

What is the difference between React context and React Redux?

You can use Context in your application directly and is going to be great for passing down data to deeply nested components which what it was designed for.

Whereas Redux is much more powerful and provides a large number of features that the Context API doesn’t provide. Also, React Redux uses context internally but it doesn’t expose this fact in the public API.

Why are Redux state functions called reducers?

Reducers always return the accumulation of the state (based on all previous and current actions). Therefore, they act as a reducer of state. Each time a Redux reducer is called, the state and action are passed as parameters. This state is then reduced (or accumulated) based on the action, and then the next state is returned. You could reduce a collection of actions and an initial state (of the store) on which to perform these actions to get the resulting final state.

How do you make AJAX request in Redux?

You can use redux-thunk middleware which allows you to define async actions.

Let’s take an example of fetching specific account as an AJAX call using fetch API:

export function fetchAccount(id) {
return (dispatch) => {
dispatch(setLoadingAccountState()); // Show a loading spinner
fetch(`/account/${id}`, (response) => {
dispatch(doneFetchingAccount()); // Hide loading spinner
if (response.status === 200) {
dispatch(setAccount(response.json)); // Use a normal function to set the received state
} else {
dispatch(someError);
}
});
};
}
function setAccount(data) {
return { type: "SET_Account", data: data };
}

Should I keep all component’s state in Redux store?

Keep your data in the Redux store, and the UI related state internally in the component.

What is the proper way to access Redux store?

The best way to access your store in a component is to use the connect() function, that creates a new component that wraps around your existing one. This pattern is called Higher-Order Components, and is generally the preferred way of extending a component’s functionality in React. This allows you to map state and action creators to your component, and have them passed in automatically as your store updates.

Let’s take an example of <FilterLink> component using connect:

import { connect } from "react-redux";
import { setVisibilityFilter } from "../actions";
import Link from "../components/Link";
const mapStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.visibilityFilter,
});
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => dispatch(setVisibilityFilter(ownProps.filter)),
});
const FilterLink = connect(mapStateToProps, mapDispatchToProps)(Link);
export default FilterLink;

Due to it having quite a few performance optimizations and generally being less likely to cause bugs, the Redux developers almost always recommend using connect() over accessing the store directly (using context API).

function MyComponent {
someMethod() {
doSomethingWith(this.context.store);
}
}

What is the difference between component and container in React Redux?

Component is a class or function component that describes the presentational part of your application.

Container is an informal term for a component that is connected to a Redux store. Containers subscribe to Redux state updates and dispatch actions, and they usually don’t render DOM elements; they delegate rendering to presentational child components.

What is the purpose of the constants in Redux?

Constants allows you to easily find all usages of that specific functionality across the project when you use an IDE. It also prevents you from introducing silly bugs caused by typos – in which case, you will get a ReferenceError immediately.

Normally we will save them in a single file (constants.js or actionTypes.js).

export const ADD_TODO = "ADD_TODO";
export const DELETE_TODO = "DELETE_TODO";
export const EDIT_TODO = "EDIT_TODO";
export const COMPLETE_TODO = "COMPLETE_TODO";
export const COMPLETE_ALL = "COMPLETE_ALL";
export const CLEAR_COMPLETED = "CLEAR_COMPLETED";

In Redux, you use them in two places:

  1. During action creation:

    Let’s take actions.js:

    import { ADD_TODO } from "./actionTypes";
    export function addTodo(text) {
    return { type: ADD_TODO, text };
    }
  2. In reducers:

    Let’s create reducer.js:

    import { ADD_TODO } from "./actionTypes";
    export default (state = [], action) => {
    switch (action.type) {
    case ADD_TODO:
    return [
    ...state,
    {
    text: action.text,
    completed: false,
    },
    ];
    default:
    return state;
    }
    };

What are the different ways to write mapDispatchToProps()?

There are a few ways of binding action creators to dispatch() in mapDispatchToProps().

Here are the possible options:

const mapDispatchToProps = (dispatch) => ({
action: () => dispatch(action()),
});
const mapDispatchToProps = (dispatch) => ({
action: bindActionCreators(action, dispatch),
});
const mapDispatchToProps = { action };

The third option is just a shorthand for the first one.

What is the use of the ownProps parameter in mapStateToProps() and mapDispatchToProps()?

If the ownProps parameter is specified, React Redux will pass the props that were passed to the component into your connect functions. So, if you use a connected component:

import ConnectedComponent from "./containers/ConnectedComponent";
<ConnectedComponent user={"john"} />;

The ownProps inside your mapStateToProps() and mapDispatchToProps() functions will be an object:

{
user: "john";
}

You can use this object to decide what to return from those functions.

How do you structure Redux top level directories?

Most of the applications has several top-level directories as below:

  1. Components: Used for dumb components unaware of Redux.
  2. Containers: Used for smart components connected to Redux.
  3. Actions: Used for all action creators, where file names correspond to part of the app.
  4. Reducers: Used for all reducers, where files name correspond to state key.
  5. Store: Used for store initialization.

This structure works well for small and medium size apps.

Async flows, middleware, and Redux tooling

What is redux-saga?

redux-saga is a library that aims to make side effects (asynchronous things like data fetching and impure things like accessing the browser cache) in React/Redux applications easier and better.

It is available in NPM:

Terminal window
$ npm install --save redux-saga

What is the mental model of redux-saga?

Saga is like a separate thread in your application, that’s solely responsible for side effects. redux-saga is a redux middleware, which means this thread can be started, paused and cancelled from the main application with normal Redux actions, it has access to the full Redux application state and it can dispatch Redux actions as well.

What are the differences between call() and put() in redux-saga?

Both call() and put() are effect creator functions. call() function is used to create effect description, which instructs middleware to call the promise. put() function creates an effect, which instructs middleware to dispatch an action to the store.

Let’s take example of how these effects work for fetching particular user data.

function* fetchUserSaga(action) {
// `call` function accepts rest arguments, which will be passed to `api.fetchUser` function.
// Instructing middleware to call promise, it resolved value will be assigned to `userData` variable
const userData = yield call(api.fetchUser, action.userId);
// Instructing middleware to dispatch corresponding action.
yield put({
type: "FETCH_USER_SUCCESS",
userData,
});
}

What is Redux Thunk?

Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch() and getState() as parameters.

What are the differences between redux-saga and redux-thunk?

Both Redux Thunk and Redux Saga take care of dealing with side effects. In most of the scenarios, Thunk uses Promises to deal with them, whereas Saga uses Generators. Thunk is simple to use and Promises are familiar to many developers, Sagas/Generators are more powerful but you will need to learn them. But both middleware can coexist, so you can start with Thunks and introduce Sagas when/if you need them.

What is Redux DevTools?

Redux DevTools is a live-editing time travel environment for Redux with hot reloading, action replay, and customizable UI. If you don’t want to bother with installing Redux DevTools and integrating it into your project, consider using Redux DevTools Extension for Chrome and Firefox.

What are the features of Redux DevTools?

Some of the main features of Redux DevTools are below,

  1. Let’s you inspect every state and action payload.
  2. Let’s you go back in time by cancelling actions.
  3. If you change the reducer code, each staged action will be re-evaluated.
  4. If the reducers throw, you will see during which action this happened, and what the error was.
  5. With persistState() store enhancer, you can persist debug sessions across page reloads.

What are Redux selectors and why use them?

Selectors are functions that take Redux state as an argument and return some data to pass to the component.

For example, to get user details from the state:

const getUserData = (state) => state.user.data;

These selectors have two main benefits,

  1. The selector can compute derived data, allowing Redux to store the minimal possible state
  2. The selector is not recomputed unless one of its arguments changes

What is Redux Form?

Redux Form works with React and Redux to enable a form in React to use Redux to store all of its state. Redux Form can be used with raw HTML5 inputs, but it also works very well with common UI frameworks like Material UI, React Widgets and React Bootstrap.

What are the main features of Redux Form?

Some of the main features of Redux Form are:

  1. Field values persistence via Redux store.
  2. Validation (sync/async) and submission.
  3. Formatting, parsing and normalization of field values.

How do you add multiple middlewares to Redux?

You can use applyMiddleware().

For example, you can add redux-thunk and logger passing them as arguments to applyMiddleware():

import { createStore, applyMiddleware } from "redux";
const createStoreWithMiddleware = applyMiddleware(
ReduxThunk,
logger
)(createStore);

How do you set initial state in Redux?

You need to pass initial state as second argument to createStore:

const rootReducer = combineReducers({
todos: todos,
visibilityFilter: visibilityFilter,
});
const initialState = {
todos: [{ id: 123, name: "example", completed: false }],
};
const store = createStore(rootReducer, initialState);

How Relay is different from Redux?

Relay is similar to Redux in that they both use a single store. The main difference is that relay only manages state originated from the server, and all access to the state is used via GraphQL queries (for reading data) and mutations (for changing data). Relay caches the data for you and optimizes data fetching for you, by fetching only changed data and nothing more.

What is an action in Redux?

Actions are plain JavaScript objects or payloads of information that send data from your application to your store. They are the only source of information for the store. Actions must have a type property that indicates the type of action being performed.

For example, let’s take an action which represents adding a new todo item:

{
type: ADD_TODO,
text: 'Add todo item'
}

Comparing Redux with nearby alternatives

What are the differences between Flux and Redux?

Here are the major differences between Flux and Redux

FluxRedux
State is mutableState is immutable
The Store contains both state and change logicThe Store and change logic are separate
There are multiple stores existThere is only one store exist
All the stores are disconnected and flatSingle store with hierarchical reducers
It has a singleton dispatcherThere is no concept of dispatcher
React components subscribe to the storeContainer components uses connect function

What is the purpose of default value in context?

The defaultValue argument is only used when a component does not have a matching Provider above it in the tree. This can be helpful for testing components in isolation without wrapping them.

Below code snippet provides default theme value as Luna.

const MyContext = React.createContext(defaultValue);

What is formik?

Formik is a small react form library that helps you with the three major problems,

  1. Getting values in and out of form state
  2. Validation and error messages
  3. Handling form submission

What are typical middleware choices for handling asynchronous calls in Redux?

Some of the popular middleware choices for handling asynchronous calls in Redux eco system are Redux Thunk, Redux Promise, Redux Saga.

What is MobX?

MobX is a simple, scalable and battle tested state management solution for applying functional reactive programming (TFRP). For ReactJS application, you need to install below packages,

Terminal window
npm install mobx --save
npm install mobx-react --save

What are the differences between Redux and MobX?

Here are the main differences between Redux and MobX,

TopicReduxMobX
DefinitionIt is a javascript library for managing the application stateIt is a library for reactively managing the state of your applications
ProgrammingIt is mainly written in ES6It is written in JavaScript(ES5)
Data StoreThere is only one large store exist for data storageThere is more than one store for storage
UsageMainly used for large and complex applicationsUsed for simple applications
PerformanceNeed to be improvedProvides better performance
How it storesUses JS Object to storeUses observable to store the data

How do you make sure that user remains authenticated on page refresh while using Context API State Management?

When a user logs in and reload, to persist the state generally we add the load user action in the useEffect hooks in the main App.js. While using Redux, loadUser action can be easily accessed.

App.js

import { loadUser } from "../actions/auth";
store.dispatch(loadUser());
  • But while using Context API, to access context in App.js, wrap the AuthState in index.js so that App.js can access the auth context. Now whenever the page reloads, no matter what route you are on, the user will be authenticated as loadUser action will be triggered on each re-render.

index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import AuthState from "./context/auth/AuthState";
ReactDOM.render(
<React.StrictMode>
<AuthState>
<App />
</AuthState>
</React.StrictMode>,
document.getElementById("root")
);

App.js

const authContext = useContext(AuthContext);
const { loadUser } = authContext;
useEffect(() => {
loadUser();
}, []);

loadUser

const loadUser = async () => {
const token = sessionStorage.getItem("token");
if (!token) {
dispatch({
type: ERROR,
});
}
setAuthToken(token);
try {
const res = await axios("/api/auth");
dispatch({
type: USER_LOADED,
payload: res.data.data,
});
} catch (err) {
console.error(err);
}
};

Immutable state updates and reducer discipline

What is prop drilling?

Prop Drilling is the process by which you pass data from one component of the React Component tree to another by going through other components that do not need the data but only help in passing it around.

How do you prevent mutating array variables?

The preexisting variables outside of the function scope including state, props and context leads to a mutation and they result in unpredictable bugs during the rendering stage. The following points should be taken care while working with arrays variables.

  1. You need to take copy of the original array and perform array operations on it for the rendering purpose. This is called local mutation.
  2. Avoid triggering mutation methods such as push, pop, sort and reverse methods on original array. It is safe to use filter, map and slice method because they create a new array.

How do you update objects inside state?

You cannot update the objects which exists in the state directly. Instead, you should create a fresh new object (or copy from the existing object) and update the latest state using the newly created object. Eventhough JavaScript objects are mutable, you need to treat objects inside state as read-only while updating the state.

Let’s see this comparison with an example. The issue with regular object mutation approach can be described by updating the user details fields of Profile component. The properties of Profile component such as firstName, lastName and age details mutated in an event handler as shown below.

import { useState } from "react";
export default function Profile() {
const [user, setUser] = useState({
firstName: "John",
lastName: "Abraham",
age: 30,
});
function handleFirstNameChange(e) {
user.firstName = e.target.value;
}
function handleLastNameChange(e) {
user.lastName = e.target.value;
}
function handleAgeChange(e) {
user.age = e.target.value;
}
return (
<>
<label>
First name:
<input value={user.firstName} onChange={handleFirstNameChange} />
</label>
<label>
Last name:
<input value={user.lastName} onChange={handleLastNameChange} />
</label>
<label>
Age:
<input value={user.age} onChange={handleAgeChange} />
</label>
<p>
Profile:
{person.firstName} {person.lastName} ({person.age})
</p>
</>
);
}

Once you run the application with above user profile component, you can observe that user profile details won’t be update upon entering the input fields. This issue can be fixed by creating a new copy of object which includes existing properties through spread syntax(…obj) and add changed values in a single event handler itself as shown below.

handleProfileChange(e) {
setUser({
...user,
[e.target.name]: e.target.value
});
}

The above event handler is concise instead of maintaining separate event handler for each field. Now, UI displays the updated field values as expected without an issue.

How do you update nested objects inside state?

You cannot simply use spread syntax for all kinds of objects inside state. Because spread syntax is shallow and it copies properties for one level deep only. If the object has nested object structure, UI doesn’t work as expected with regular JavaScript nested property mutation. Let’s demonstrate this behavior with an example of User object which has address nested object inside of it.

const user = {
name: "John",
age: 32,
address: {
country: "Singapore",
postalCode: 440004,
},
};

If you try to update the country nested field in a regular javascript fashion(as shown below) then user profile screen won’t be updated with latest value.

user.address.country = "Germany";

This issue can be fixed by flattening all the fields into a top-level object or create a new object for each nested object and point it to it’s parent object. In this example, first you need to create copy of address object and update it with the latest value. Later, the address object should be linked to parent user object something like below.

setUser({
...user,
address: {
...user.address,
country: "Germany",
},
});

This approach is bit verbose and not easy for deep hierarchical state updates. But this workaround can be used for few levels of nested objects without much hassle.

How do you update arrays inside state?

Eventhough arrays in JavaScript are mutable in nature, you need to treat them as immutable while storing them in a state. That means, similar to objects, the arrays cannot be updated directly inside state. Instead, you need to create a copy of the existing array and then set the state to use newly copied array.

To ensure that arrays are not mutated, the mutation operations like direct direct assignment(arr[1]=β€˜one’), push, pop, shift, unshift, splice etc methods should be avoided on original array. Instead, you can create a copy of existing array with help of array operations such as filter, map, slice, spread syntax etc.

For example, the below push operation doesn’t add the new todo to the total todo’s list in an event handler.

onClick = {
todos.push({
id: id+1,
name: name
})
}

This issue is fixed by replacing push operation with spread syntax where it will create a new array and the UI updated with new todo.

onClick = {
[
...todos,
{ id: id+1, name: name }
]
}

How do you use immer library for state updates?

Immer library enforces the immutability of state based on copy-on-write mechanism. It uses JavaScript proxy to keep track of updates to immutable states. Immer has 3 main states as below,

  1. Current state: It refers to actual state
  2. Draft state: All new changes will be applied to this state. In this state, draft is just a proxy of the current state.
  3. Next state: It is formed after all mutations applied to the draft state

Immer can be used by following below instructions,

  1. Install the dependency using npm install use-immer command
  2. Replace useState hook with useImmer hook by importing at the top
  3. The setter function of useImmer hook can be used to update the state.

For example, the mutation syntax of immer library simplifies the nested address object of user state as follows,

import { useImmer } from "use-immer";
const [user, setUser] = useImmer({
name: "John",
age: 32,
address: {
country: "Singapore",
postalCode: 440004,
},
});
//Update user details upon any event
setUser((draft) => {
draft.address.country = "Germany";
});

The preceding code enables you to update nested objects with a conceise mutation syntax.

What are the benefits of preventing the direct state mutations?

What are the preferred and non-preferred array operations for updating the state?

The following table represent preferred and non-preferred array operations for updating the component state.

ActionPreferredNon-preferred
Addingconcat, […arr]push, unshift
Removingfilter, slicepop, shift, splice
Replacingmapsplice, arr[i] = someValue
sortingcopying to new arrayreverse, sort

If you use Immer library then you can able to use all array methods without any problem.

What are the guidelines to be followed for writing reducers?

There are two guidelines to be taken care while writing reducers in your code.

  1. Reducers must be pure without mutating the state. That means, same input always returns the same output. These reducers run during rendering time similar to state updater functions. So these functions should not send any requests, schedule time outs and any other side effects.

  2. Each action should describe a single user interaction even though there are multiple changes applied to data. For example, if you β€œreset” registration form which has many user input fields managed by a reducer, it is suggested to send one β€œreset” action instead of creating separate action for each fields. The proper ordering of actions should reflect the user interactions in the browser and it helps a lot for debugging purpose.

Modern React state with context and hooks

How is useReducer Different from useState?

There are notable differences between useState and useReducer hooks.

FeatureuseStateuseReducer
State complexitySimple (one variable or flat object)Complex, multi-part or deeply nested
Update styleDirect (e.g. setState(x))Through actions (e.g. dispatch({}))
Update logicIn componentIn reducer function
Reusability & testingLess reusableHighly reusable & testable

What is useContext? What are the steps to follow for useContext?

The useContext hook is a built-in React Hook that lets you access the value of a context inside a functional component without needing to wrap it in a <Context.Consumer> component.

It helps you avoid prop drilling (passing props through multiple levels) by allowing components to access shared data like themes, authentication status, or user preferences.

The usage of useContext involves three main steps:

Step 1 : Create the Context

Use React.createContext() to create a context object.

import React, { createContext } from 'react';
const ThemeContext = createContext(); // default value optional

You typically export this so other components can import it.

Step 2: Provide the Context Value

Wrap your component tree (or a part of it) with the Context.Provider and pass a value prop.

function App() {
return (
<ThemeContext.Provider value="dark">
<MyComponent />
</ThemeContext.Provider>
);
}

Now any component inside <ThemeContext.Provider> can access the context value.

Step 3: Consume the Context with **useContext**

In any functional component inside the Provider, use the useContext hook:

import { useContext } from 'react';
function MyComponent() {
const theme = useContext(ThemeContext); // theme = "dark"
return <p>Current Theme: {theme}</p>;
}

What are the use cases of useContext hook?

The useContext hook in React is used to share data across components without having to pass props manually through each level. Here are some common and effective use cases:

  1. Theme Customization
    useContext can be used to manage application-wide themes, such as light and dark modes, ensuring consistent styling and enabling user-driven customization.
  2. Localization and Internationalization
    It supports localization by providing translated strings or locale-specific content to components, adapting the application for users in different regions.
  3. User Authentication and Session Management
    useContext allows global access to authentication status and user data. This enables conditional rendering of components and helps manage protected routes or user-specific UI elements.
  4. Shared Modal or Sidebar Visibility
    It’s ideal for managing the visibility of shared UI components like modals, drawers, or sidebars, especially when their state needs to be controlled from various parts of the app.
  5. Combining with **useReducer** for Global State Management
    When combined with useReducer, useContext becomes a powerful tool for managing more complex global state logic. This pattern helps maintain cleaner, scalable state logic without introducing external libraries like Redux. Some of the common use cases of useContext are listed below,

What is useReducer? Why do you use useReducer?

The useReducer hook is a React hook used to manage complex state logic inside functional components. It is conceptually similar to Redux. i.e, Instead of directly updating state like with useState, you dispatch an action to a reducer function, and the reducer returns the new state.

The useReducer hook takes three arguments:

const [state, dispatch] = useReducer(reducer, initialState, initFunction);
  • **reducer**: A function (state, action) => newState that handles how state should change based on the action.
  • **initialState**: The starting state.
  • **dispatch**: A function you call to trigger an update by passing an action.

The useReducer hook is used when:

  • The state is complex, such as nested structures or multiple related values.
  • State updates depend on the previous state and logic.
  • You want to separate state update logic from UI code to make it cleaner and testable.
  • You’re managing features like:
    • Forms
    • Wizards / Multi-step flows
    • Undo/Redo functionality
    • Shopping cart logic
    • Toggle & conditional UI logic

How does useReducer works? Explain with an example

The useReducer hooks works similarly to Redux, where:

  • You define a reducer function to handle state transitions.
  • You dispatch actions to update the state.

Counter Example with Increment, Decrement, and Reset:

  1. Reducer function:

    Define a counter reducer function that takes the current state and an action object with a type, and returns a new state based on that type.

    function counterReducer(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:
    return state;
    }
    }
  2. Using useReducer: Invoke useReducer with above reducer function along with initial state. Thereafter, you can attach dispatch actions for respective button handlers.

import React, { useReducer } from 'react';
function Counter() {
const initialState = { count: 0 };
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div style={{ textAlign: 'center' }}>
<h2>Count: {state.count}</h2>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
export default Counter;

Once the new state has been returned, React re-renders the component with the updated state.count.

Can you combine useReducer with useContext?

Yes, it’s common to combine useReducer with useContext to build a lightweight state management system similar to Redux:

const AppContext = React.createContext();
function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}

Can you dispatch multiple actions in a row with useReducer?

Yes, you can dispatch multiple actions in a row using useReducer but not directly in one call. You’d have to call dispatch multiple times or create a composite action in your reducer that performs multiple updates based on the action type.

Example: Dispatching Multiple Actions You can define a custom function with dispatching actions one by one.

function handleMultipleActions(dispatch) {
dispatch({ type: 'increment' });
dispatch({ type: 'increment' });
dispatch({ type: 'reset' });
}

After that, you need to invoke it through event handler

<button onClick={() => handleMultipleActions(dispatch)}>
Run Multiple Actions
</button>

Note: You can also define a custom action type If you want multiple state changes to be handled in one reducer call.

case 'increment_twice':
return { count: state.count + 2 };

Then dispatch

dispatch({ type: 'increment_twice' });

Is dispatch from useReducer asynchronous and does it update state immediately?

The dispatch function returned by useReducer is not asynchronous - it is a synchronous function call. When you call dispatch(action), React synchronously invokes your reducer with the current state and the action, computes the new state, and schedules a re-render. However, the state variable does not update immediately within the same render cycle. The updated state is only available in the next render.

This behavior is similar to useState’s setState - React batches state updates for performance optimization, meaning the component does not re-render immediately after each dispatch call. Instead, React processes all dispatched actions and re-renders once with the final state.

Key Points

  1. dispatch is synchronous: The reducer runs immediately when dispatch is called.
  2. State update is not immediate in the current render: The state variable still holds the old value until the next render.
  3. React batches updates: Multiple dispatch calls within the same event handler result in a single re-render.
  4. Reducer is a pure function: It computes the new state without side effects.

Example demonstrating that state does not update immediately

import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
const handleClick = () => {
dispatch({ type: 'increment' });
console.log(state.count); // Still logs the OLD value (e.g., 0), not 1
dispatch({ type: 'increment' });
console.log(state.count); // Still logs the OLD value (e.g., 0), not 2
};
// After re-render, state.count will be 2 (both dispatches are processed)
return (
<div>
<p>Count: {state.count}</p>
<button onClick={handleClick}>Increment Twice</button>
</div>
);
}

In the above example, even though dispatch is called twice, state.count still reflects the previous value inside the event handler. React batches both dispatches and re-renders the component once with count: 2.

How to read updated state after dispatch

If you need the updated value right after dispatching, you have several options:

  1. Use useEffect to react to state changes:

    useEffect(() => {
    console.log('Updated count:', state.count);
    }, [state.count]);
  2. Compute the next state manually:

    const handleClick = () => {
    const nextState = reducer(state, { type: 'increment' });
    console.log('Next state will be:', nextState.count);
    dispatch({ type: 'increment' });
    };
  3. Use useRef to track the latest state:

    const stateRef = useRef(state);
    useEffect(() => {
    stateRef.current = state;
    }, [state]);

Note: This behavior is by design in React. The dispatch function itself has a stable identity (it doesn’t change between re-renders), which makes it safe to omit from useEffect dependency arrays.

How does useContext works? Explain with an example

The useContext hook can be used for authentication state management across multiple components and pages in a React application.

Let’s build a simple authentication flow with:

  • Login and Logout buttons
  • Global AuthContext to share state
  • Components that can access and update auth status

1. Create the Auth Context:

You can define AuthProvider which holds and provides user, login(), and logout() via context.

AuthContext.js
import React, { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (username) => setUser({ name: username });
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// Custom hook for cleaner usage
export const useAuth = () => useContext(AuthContext);

2. Wrap Your App with the Provider:

Wrap the above created provider in main App.js file

App.js
import React from 'react';
import { AuthProvider } from './AuthContext';
import HomePage from './HomePage';
import Dashboard from './Dashboard';
function App() {
return (
<AuthProvider>
<HomePage />
<Dashboard />
</AuthProvider>
);
}
export default App;

3. Home page with login: Read or access user and login details through custom useAuth hook and use it inside home page.

HomePage.js
import React from 'react';
import { useAuth } from './AuthContext';
function HomePage() {
const { user, login } = useAuth();
return (
<div>
<h1>Home</h1>
{user ? (
<p>Welcome back, {user.name}!</p>
) : (
<button onClick={() => login('Alice')}>Login</button>
)}
</div>
);
}
export default HomePage;

4. Dashboard with logout: Read or access user and logout details from useAuth custom hook and use it inside dashboard page.

Dashboard.js
import React from 'react';
import { useAuth } from './AuthContext';
function Dashboard() {
const { user, logout } = useAuth();
if (!user) {
return <p>Please login to view the dashboard.</p>;
}
return (
<div>
<h2>Dashboard</h2>
<p>Logged in as: {user.name}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
export default Dashboard;

Can You Use Multiple Contexts in One Component?

Yes, it is possible. You can use multiple contexts inside the same component by calling useContext multiple times, once for each context.

It can be achieved with below steps,

  • Create multiple contexts using createContext().
  • Wrap your component tree with multiple <Provider>s.
  • Call useContext() separately for each context in the same component.

Example: Using ThemeContext and UserContext Together

import React, { createContext, useContext } from 'react';
// Step 1: Create two contexts
const ThemeContext = createContext();
const UserContext = createContext();
function Dashboard() {
// Step 2: Use both contexts
const theme = useContext(ThemeContext);
const user = useContext(UserContext);
return (
<div style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
<h1>Welcome, {user.name}</h1>
<p>Current theme: {theme}</p>
</div>
);
}
// Step 3: Provide both contexts
function App() {
return (
<ThemeContext.Provider value="dark">
<UserContext.Provider value={{ name: 'Sudheer' }}>
<Dashboard />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
export default App;

What’s a common pitfall when using useContext with objects?

A common pitfall when using useContext with objects is triggering unnecessary re-renders across all consuming components - even when only part of the context value changes.

When you provide an object as the context value, React compares the entire object reference. If the object changes (even slightly), React assumes the whole context has changed, and all components using useContext(MyContext) will re-render, regardless of whether they use the part that changed.

Example:

const MyContext = React.createContext();
function MyProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// This causes all consumers to re-render on any state change
const contextValue = { user, setUser, theme, setTheme };
return (
<MyContext.Provider value={contextValue}>
{children}
</MyContext.Provider>
);
}

In this case, a change in theme will also trigger a re-render in components that only care about user.

This issue can be fixed in two ways,

1. Split Contexts
Create separate contexts for unrelated pieces of state:

const UserContext = React.createContext();
const ThemeContext = React.createContext();

2. Memoize Context Value
Use useMemo to prevent unnecessary re-renders:

const contextValue = useMemo(() => ({ user, setUser, theme, setTheme }), [user, theme]);

However, this only helps if the object structure and dependencies are well controlled.

Share & Connect