What is State Management?
What "state" is, why two parts of an app start to disagree about the same data, and the three questions every state management tool - including Redux - answers.
What you will be able to do
- Explain what "state" means in a frontend app, with examples
- Show, with running code, how two copies of the same data drift apart
- Name the three questions of state management: where it lives, who may change it and how, and how everyone finds out
- Fix the bug two ways: one owner in React, and one shared store
- Read a first Redux Toolkit example and name its parts: store, action, reducer, dispatch, subscribe
- Explain why Redux state cannot be changed directly
- Say when an app does not need Redux at all
The idea, in plain English
This is the first lesson of the Redux course, and it has almost no Redux in it. Redux is an answer, and an answer only makes sense when you know the question. The question is: how do we manage state?
State is data that can change while the app is running, and that the screen depends on. The logged-in user, the items in a cart, whether dark mode is on, the text typed into a search box - all of these are state. When state changes, the screen must change with it.
In a small app this is easy. In a bigger app the same data is needed in many places: the header shows the user’s name, the profile page lets them edit it, the settings page shows their plan. If each place keeps its own copy, the copies drift apart, and two screens show different answers to the same question. That bug is the whole reason state management exists.
In this lesson you will see that bug happen, in plain JavaScript and in React, and fix it twice. At the end you will see the same fix written with Redux Toolkit. All code was run with Node 22, React 19.3, Redux Toolkit 2.13.0 and Redux 5.0.1.
Worked example: Two screens disagreeing about the same user: the header still says "free" after the user upgraded on the profile page.
1 - The app loads
Both parts copy the user from the server. Right now the two copies are the same, so everything looks fine.
The header and the profile page each copy the user when the app loads. A real run of Example 1 below.
1 - The click asks the store to change
The profile page does not change any copy of its own. It asks the store: please set plan to "pro". The store is the only place that holds the user.
The same upgrade with one shared store - Example 3 below. There is only one copy, and the store tells everyone when it changes.
Data moves in one direction only. You will meet each box properly in Modules 2 and 3.
What is "state"?
In a frontend app, state is any data that can change while the app is running, and that the screen depends on. If the data changes, something on the screen should look different.
A simple test: if the value changes, must the screen change? If yes, it is state. The app’s name in the logo never changes, so it is not state. The number on the cart badge changes every time you add an item, so it is state.
Logged-in userName in the header, avatar, plan. Changes when you log in, log out, or upgrade.Saved lessonsThe bookmark count in the header. Changes when you save or remove a lesson.ThemeDay, read or night. Changes when you press the theme button.Search textWhat you typed in the course search box. Changes with every key.Is the menu open?true or false. Changes when you tap the menu.List of coursesLoaded from the server. Changes when new courses are added.Lesson progress"3 of 20 complete". Changes when you finish a lesson.An everyday picture: the classroom whiteboard
Imagine a class of 40 students. On Monday the teacher says, "The exam is on Friday", and every student writes it in their own notebook. On Wednesday the teacher moves the exam to next Monday and tells only the students in the first row. Now 40 notebooks hold two different exam dates. Some students will come on Friday.
Now imagine a different class. The exam date is written on one whiteboard at the front. Students do not copy it - they look at the board. Only the teacher may change what is on the board. And when the teacher changes it, a bell rings so everyone looks up. Every student always has the right date.
The notebooks are what happens when every part of an app keeps its own copy of the data. The whiteboard is state management: one place for the data, clear rules about who may change it, and a way to tell everyone when it changes.
The exam dateA piece of state, like the user’s plan.Each student’s notebookEach component keeping its own copy - the copies drift apart.The whiteboardOne shared place for the data (in Redux: the store).Only the teacher writesClear rules about how the data may change (in Redux: actions and reducers).The bellTelling everyone about a change (in Redux: subscribe).Example 1: two copies drift apart
Here is the bug in plain JavaScript, with no framework. The header and the profile page each make their own copy of the user from the server. Then the user upgrades on the profile page.
Read the output carefully. After loading, both say "free". After the upgrade, the profile page says "pro" - but the header still says "free". Even the server was updated. The header simply never found out, because it holds a separate copy and nothing told it.
// Two parts of an app each keep their OWN copy of the same user.
const server = { user: { name: "Ravi", plan: "free" } };
const header = { user: { ...server.user } }; // copy 1
const profilePage = { user: { ...server.user } }; // copy 2
function showHeader() {
console.log(`Header: ${header.user.name} (${header.user.plan})`);
}
function showProfile() {
console.log(`Profile: ${profilePage.user.name} (${profilePage.user.plan})`);
}
console.log("--- after loading");
showHeader();
showProfile();
// The user upgrades on the profile page. Only that copy changes.
profilePage.user.plan = "pro";
server.user.plan = "pro";
console.log("--- after upgrading on the profile page");
showHeader();
showProfile();--- after loading
Header: Ravi (free)
Profile: Ravi (free)
--- after upgrading on the profile page
Header: Ravi (free) <- wrong: still the old copy
Profile: Ravi (pro)The same bug in React
React apps get this bug the same way. Below, Header and ProfilePage each call useState with the user. useState gives each component its own private copy. Clicking Upgrade changes the ProfilePage copy only.
We rendered this component in a test browser (jsdom) and clicked the button. Before the click: "Header: Ravi (free)" and "Profile: Ravi (free)". After the click: "Header: Ravi (free)" and "Profile: Ravi (pro)". Two screens, two answers.
import { useState } from "react";
const userFromServer = { name: "Ravi", plan: "free" };
function Header() {
const [user] = useState(userFromServer); // copy 1
return <header>Header: {user.name} ({user.plan})</header>;
}
function ProfilePage() {
const [user, setUser] = useState(userFromServer); // copy 2
return (
<section>
Profile: {user.name} ({user.plan})
<button onClick={() => setUser({ ...user, plan: "pro" })}>Upgrade</button>
</section>
);
}
export default function App() {
return (
<>
<Header />
<ProfilePage />
</>
);
}before click: Header: Ravi (free) / Profile: Ravi (free)
after click: Header: Ravi (free) / Profile: Ravi (pro) <- they disagreeFix 1: one copy, one owner
The simplest fix in React is to keep one copy, in a component above both - here App - and pass it down as props. Now there is only one user object. When it changes, React re-renders both children with the new value. This is called lifting state up, and Lesson 6 covers it in detail.
We rendered and clicked this version too. After the click both said "Ravi (pro)". This fix is perfect for small apps. Lessons 5 and 6 show where it starts to hurt: when the owner is far away from the components that need the data, every component in between must pass it along.
import { useState } from "react";
function Header({ user }) {
return <header>Header: {user.name} ({user.plan})</header>;
}
function ProfilePage({ user, onUpgrade }) {
return (
<section>
Profile: {user.name} ({user.plan})
<button onClick={onUpgrade}>Upgrade</button>
</section>
);
}
export default function App() {
// ONE copy, owned by the parent, passed down to both
const [user, setUser] = useState({ name: "Ravi", plan: "free" });
return (
<>
<Header user={user} />
<ProfilePage user={user} onUpgrade={() => setUser({ ...user, plan: "pro" })} />
</>
);
}before click: Header: Ravi (free) / Profile: Ravi (free)
after click: Header: Ravi (pro) / Profile: Ravi (pro) <- always agreeThe three questions of state management
Every state management tool - useState, React Context, Zustand, Redux - is a different answer to the same three questions. When you meet a new tool, ask these three questions and you will understand it quickly.
In Fix 1, the answers were: the data lives in App; only App can change it, through setUser; React re-renders the children when it changes. In Redux, as you will see below, the answers are: the data lives in the store; it changes only through dispatched actions handled by reducers; components subscribe to the store.
1. Where does it live?Which one place holds the data? (A component, a context, a store.)2. Who may change it, and how?Can any code change it, or only through specific functions? Can you see every change?3. How does everyone find out?When it changes, how do the parts of the screen that use it update?Example 2: one shared store, in 20 lines
Now let us build the whiteboard ourselves, in plain JavaScript. createStore keeps the state in one variable. getState lets anyone read it. setState is the only way to change it. subscribe lets any part of the app say "call me when it changes".
Look at how the three questions are answered. Where does it live? In the state variable inside createStore - one copy. Who may change it? Only setState. How does everyone find out? setState calls every subscribed listener. When the user upgrades, both the header and the profile page print "pro".
This tiny store is the core idea of Redux. Real Redux adds strict rules on top of it - for example, that every change is described by an "action" object, so you can log and replay every change. In Module 3 you will use Redux’s own createStore by hand, without Toolkit.
// ONE place holds the user. Everyone reads from it and is told when it changes.
function createStore(initialState) {
let state = initialState;
const listeners = [];
return {
getState: () => state, // anyone may read
setState(changes) { // one way to change it
state = { ...state, ...changes };
listeners.forEach((listener) => listener());
},
subscribe: (listener) => listeners.push(listener),
};
}
const store = createStore({ user: { name: "Ravi", plan: "free" } });
function showHeader() {
const { user } = store.getState();
console.log(`Header: ${user.name} (${user.plan})`);
}
function showProfile() {
const { user } = store.getState();
console.log(`Profile: ${user.name} (${user.plan})`);
}
store.subscribe(showHeader); // "tell me when the user changes"
store.subscribe(showProfile);
console.log("--- the user upgrades");
store.setState({ user: { ...store.getState().user, plan: "pro" } });--- the user upgrades
Header: Ravi (pro)
Profile: Ravi (pro)Example 3: the same idea with Redux Toolkit
Here is a first look at real Redux. Do not try to learn every line yet - Modules 2 to 4 explain them. Just find the three answers. Where does the state live? In the store made by configureStore. Who may change it, and how? Only the reducers in the slice, and only when you dispatch an action. How does everyone find out? store.subscribe.
Two details are worth noticing now. First, an action is just a plain object that describes what happened: { type: "user/upgraded" }. Second, you cannot change the state directly. When we tried store.getState().user.plan = "enterprise", Redux Toolkit refused with TypeError: Cannot assign to read only property ‘plan’. The state is frozen, so every change must go through dispatch. That rule is what makes Redux predictable.
// npm install @reduxjs/toolkit
import { configureStore, createSlice } from "@reduxjs/toolkit";
// A "slice": one part of the state, and the ways it is allowed to change
const userSlice = createSlice({
name: "user",
initialState: { name: "Ravi", plan: "free" },
reducers: {
upgraded(state) {
state.plan = "pro";
},
renamed(state, action) {
state.name = action.payload;
},
},
});
// The store: the one place where the app's shared state lives
const store = configureStore({ reducer: { user: userSlice.reducer } });
store.subscribe(() => {
console.log("store changed ->", store.getState().user);
});
const { upgraded, renamed } = userSlice.actions;
console.log("action object:", upgraded());
store.dispatch(upgraded()); // "please upgrade the user"
store.dispatch(renamed("Ravi K")); // "please rename the user"
// Changing the state directly is not allowed:
try {
store.getState().user.plan = "enterprise";
} catch (error) {
console.log("direct change ->", error.constructor.name + ":", error.message);
}action object: { type: 'user/upgraded', payload: undefined }
store changed -> { name: 'Ravi', plan: 'pro' }
store changed -> { name: 'Ravi K', plan: 'pro' }
direct change -> TypeError: Cannot assign to read only property 'plan' of object '#<Object>'Watch out: The TypeError appears in ES modules (import syntax), which is what React projects use. In an old-style script (require, no "use strict"), JavaScript ignores the write without any error - we checked: the plan silently stayed "free". Either way the state did not change, and the rule is the same: change state only with dispatch.
Words you will see in this course
You do not need to remember these yet. Come back to this table when a word is not clear.
StateData that can change while the app runs, and that the screen depends on.Single source of truthEach piece of data lives in exactly one place. Everyone reads it from there.StoreIn Redux, the one object that holds the shared state.ActionA plain object that describes what happened: { type: "user/upgraded" }.DispatchSend an action to the store: store.dispatch(action).ReducerA function that takes the current state and an action, and returns the new state.SubscribeAsk the store to call you whenever the state changes.SliceIn Redux Toolkit, one part of the state (like user) plus its reducers and actions.StaleOld and out of date - like the header still showing "free".Why it gets harder as the app grows
In a three-component example, you can see every copy of the data at a glance. Real apps have hundreds of components, written by many developers over years. Three things then go wrong.
Duplication: the same data is stored in several places, often without anyone deciding to - a component copies a prop into its own useState "just for now". Drift: the copies stop agreeing, as in Example 1, and the bug shows up far from its cause. Unclear ownership: when a value is wrong, nobody knows which code changed it, because any code could.
Lesson 4 looks at these failure modes in detail. Every tool in this course exists to prevent one of them.
Different kinds of state
Not all state is the same, and that changes which tool fits. Two splits matter most, and each has its own lesson next.
Local or global (Lesson 2): is the data used by one component, or by many parts of the app? Client or server (Lesson 3): is it data your app owns, like "is the menu open", or a copy of data that really lives on a server, like the course list? Most state is local, and most "global" state is really server data - which is why many apps need much less Redux than people think.
Is this dropdown open?Local, client state. Keep it in the component with useState.Text typed in a formUsually local, client state.Theme (day / night)Global, client state. Many components read it.Logged-in userGlobal. It comes from the server, but the whole app reads it.List of coursesServer state - a cached copy of the server’s data (Module 10, RTK Query).Do you always need Redux?
No. If your state is mostly local, or only a few components share it, useState and lifting state up (Fix 1) are enough. If the shared data is mostly from the server, a data-fetching tool may be the better answer. Lesson 8 gives honest criteria.
Redux earns its place when many parts of a large app share and change the same client data, when you need to see and trace every change, or when a team needs one clear pattern everyone follows. This course teaches Redux properly, and it also teaches you when not to use it.
Common questions
Is Redux only for React? No. Redux itself is plain JavaScript - Example 3 ran in Node with no React at all. The react-redux package connects it to React components (Module 5).
Is Redux the same as Redux Toolkit? Redux Toolkit is the official, modern way to write Redux. It uses Redux inside, and removes most of the repeated code. This course shows plain Redux first (Module 3) so that Toolkit (Module 4) makes sense.
Is state the same as data in a database? No. The database is the permanent record on the server. State is what the app holds in memory right now, while it runs. When you reload the page, client state starts again from the beginning.
This lesson at a glance
StateChanging data the screen depends on.
The bugTwo copies of the same data drift apart.
useState(user) in two components
Fix 1: one ownerKeep one copy in a parent, pass it down.
<Header user={user} />Fix 2: one storeKeep one copy in a store, everyone subscribes.
store.subscribe(listener)
Redux storeMade by Redux Toolkit.
configureStore({ reducer })Change stateOnly by dispatching an action.
store.dispatch(upgraded())
Read stateGet the current value.
store.getState().user
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Run drift.js. Then change header.user.name = "Ravi K" instead of the plan. Which screen is now stale?”
“In store.js, add showSettings() that prints the plan, and subscribe it. Upgrade once and count the lines.”
“In user-store.mjs, print each action before dispatching it. What is the type of renamed("Ravi K")?”
“Open any app on your phone - food delivery, banking. List five pieces of state you can see on one screen.”
What usually goes wrong
Every copy can drift. Keep one copy and pass it down, or read it from one store.
✗ function Header() {
const [user] = useState(userFromServer);✓ function Header({ user }) { // one copy, owned aboveChanging an object in place does not tell the screens that use it. Change it through the owner (setUser, setState, dispatch).
✗ profilePage.user.plan = "pro";✓ store.setState({ user: { ...user, plan: "pro" } });Redux Toolkit freezes the state. Direct writes throw a TypeError in modules, or are ignored in old scripts. Dispatch an action instead.
✗ store.getState().user.plan = "pro";✓ store.dispatch(upgraded());Whether a dropdown is open is nobody else’s business. Keep local state local - Lesson 2.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
In store.js, add a cart to the state: cart: [] . Add a listener showCartBadge that prints "Cart: N items", and add two items with setState. What does the listener print?
Show hintHide hint
setState({ cart: [...store.getState().cart, "Python course"] }) builds a new array with one more item.
Show solutionHide solution
const cartStore = createStore({ cart: [] });
cartStore.subscribe(() => {
console.log("Cart:", cartStore.getState().cart.length, "items");
});
cartStore.setState({ cart: [...cartStore.getState().cart, "Python course"] });
cartStore.setState({ cart: [...cartStore.getState().cart, "Redux course"] });
// Cart: 1 items
// Cart: 2 itemsFix drift.js without a store: make the header and the profile page share ONE user object instead of two copies. What does it print now?
Show hintHide hint
The copies were made by { ...server.user }. What if both pointed at the same object?
Show solutionHide solution
const user = { name: "Ravi", plan: "free" }; // one object
const header = { user }; // both point at the SAME object
const profilePage = { user };
profilePage.user.plan = "pro";
console.log("Header:", header.user.plan); // Header: pro
console.log("Profile:", profilePage.user.plan); // Profile: pro
// The data now agrees - but nothing TOLD the header to redraw.
// In a real UI the header would still show "free" until something
// re-renders it. That is why stores also need subscribe().Look at the "State in a course website" table. For each row, say whether it is local or global, and whether it comes from the server.
Show hintHide hint
Ask: how many parts of the page use it? And: would it still be true if I opened the site on another device?
Show solutionHide solution
Logged-in user global from the server
Saved lessons global from the server
Theme global client (saved in this browser)
Search text local client
Is the menu open? local client
List of courses global from the server
Lesson progress global from the serverBuild a better tiny store
Improve the createStore from Example 2 so it is closer to a real store. You will use Redux’s real createStore in Module 3; this is a warm-up.
- subscribe(listener) returns an unsubscribe function that removes that listener
- setState does nothing (and calls no listener) if the changes would not change any value
- Show that after unsubscribing, a listener is no longer called
function createStore(initialState) {
let state = initialState;
const listeners = [];
return {
getState: () => state,
setState(changes) {
// TODO: skip when nothing changes
state = { ...state, ...changes };
listeners.forEach((listener) => listener());
},
subscribe(listener) {
listeners.push(listener);
// TODO: return a function that removes this listener
},
};
}Show one solutionHide solution
function createStore(initialState) {
let state = initialState;
let listeners = [];
return {
getState: () => state,
setState(changes) {
const changed = Object.keys(changes).some((key) => state[key] !== changes[key]);
if (!changed) return; // nothing new: stay quiet
state = { ...state, ...changes };
listeners.forEach((listener) => listener());
},
subscribe(listener) {
listeners.push(listener);
return () => {
listeners = listeners.filter((item) => item !== listener);
};
},
};
}
const store = createStore({ plan: "free" });
const stop = store.subscribe(() => console.log("plan is now", store.getState().plan));
store.setState({ plan: "pro" }); // plan is now pro
store.setState({ plan: "pro" }); // (nothing - no change)
stop();
store.setState({ plan: "team" }); // (nothing - unsubscribed)
console.log("final:", store.getState().plan); // final: teamKey points
- State is data that can change while the app runs, and that the screen depends on.
- When two parts of an app keep their own copies of the same data, the copies drift apart.
- State management answers three questions: where does it live, who may change it and how, how does everyone find out.
- In React, one fix is one owner: keep the data in a parent and pass it down.
- A store keeps one copy, allows changes only through one door, and notifies subscribers.
- Redux: the store holds state, actions describe changes, reducers make the new state, components subscribe.
- Redux Toolkit freezes state - you change it only by dispatching actions.
- Not every app needs Redux; most state is local, and much "global" state is really server data.
Quick check before you move on
Interview questions
What is state management in a frontend application?
Deciding where changing data lives, who is allowed to change it and through what operations, and how the parts of the UI that depend on it are updated when it changes.
What problem does a single source of truth solve?
Drift between copies. If each piece of data lives in exactly one place and everything reads it from there, two parts of the UI can never disagree about it.
Describe the Redux data flow.
The UI dispatches an action describing what happened; the store passes the current state and the action to the reducer; the reducer returns the new state; subscribed UI components read it and re-render. Data moves in one direction.
Why does Redux forbid changing state directly?
So every change goes through dispatch and a reducer. That makes changes visible, loggable and replayable, and lets the UI detect changes by reference. Redux Toolkit enforces it by freezing the state.
When would you not use Redux?
When state is mostly local to components, when only a few nearby components share it, or when the shared data is server data better handled by a data-fetching cache.
Quiz
- 1.
Which of these is NOT state: the cart item count, the site logo, whether the menu is open, the search text?
- 2.
Two components both call useState(userFromServer). One of them updates its user. What does the other one show?
- 3.
What does the action object for upgraded() look like?
- 4.
In an ES module, what happens when you write store.getState().user.plan = "enterprise" with Redux Toolkit?
- 5.
Does every React app need Redux?
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned