← Back to Redux
Lesson 1 · State Management Fundamentals

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.

Beginner30 min

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.

request flowTwo copies drift apartstep 1 / 3

1 - The app loads

Both parts copy the user from the server. Right now the two copies are the same, so everything looks fine.

Header shows
Ravi (free)
Profile shows
Ravi (free)
copies
2
agree?
yes

The header and the profile page each copy the user when the app loads. A real run of Example 1 below.

request flowOne store, everyone reads itstep 1 / 2

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.

copies of user
1
who changes it
only the store
new plan
pro
how
store.setState(...)

The same upgrade with one shared store - Example 3 below. There is only one copy, and the store tells everyone when it changes.

ArchitecturePreview: the Redux looptap a node to trace it

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.

State in a course website like this one
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.

Classroom -> app
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.

drift.js
// 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();
Output - node drift.js
--- 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.

Bug.jsx - each component has its own copy
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 /> </> ); }
What the screen showed - rendered in jsdom, button clicked
before click: Header: Ravi (free) / Profile: Ravi (free) after click: Header: Ravi (free) / Profile: Ravi (pro) <- they disagree

Fix 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.

Fixed.jsx - the parent owns the only copy
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" })} /> </> ); }
What the screen showed
before click: Header: Ravi (free) / Profile: Ravi (free) after click: Header: Ravi (pro) / Profile: Ravi (pro) <- always agree

The 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.

Ask these about any tool
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.

store.js
// 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" } });
Output - node store.js
--- 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.

user-store.mjs
// 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); }
Output - node user-store.mjs
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.

Small dictionary
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.

Sorting some examples
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

State

Changing data the screen depends on.

The bug

Two copies of the same data drift apart.

useState(user) in two components
Fix 1: one owner

Keep one copy in a parent, pass it down.

<Header user={user} />
Fix 2: one store

Keep one copy in a store, everyone subscribes.

store.subscribe(listener)
Redux store

Made by Redux Toolkit.

configureStore({ reducer })
Change state

Only by dispatching an action.

store.dispatch(upgraded())
Read state

Get 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.

Break it yourself

“Run drift.js. Then change header.user.name = "Ravi K" instead of the plan. Which screen is now stale?”

A third listener

“In store.js, add showSettings() that prints the plan, and subscribe it. Upgrade once and count the lines.”

Watch the actions

“In user-store.mjs, print each action before dispatching it. What is the type of renamed("Ravi K")?”

Find the state

“Open any app on your phone - food delivery, banking. List five pieces of state you can see on one screen.”

What usually goes wrong

Copying shared data into each component

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 above
Changing shared data without telling anyone

Changing 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" } });
Changing Redux state directly

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());
Putting everything in global state

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.

1.

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 hint

setState({ cart: [...store.getState().cart, "Python course"] }) builds a new array with one more item.

Show solution
Solution - add to the end of store.js
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 items
2.

Fix 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 hint

The copies were made by { ...server.user }. What if both pointed at the same object?

Show solution
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().
3.

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 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 solution
One possible answer
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 server
Coding challenge

Build 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.

It should
  • 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
Starter
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 solution
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: team

Key 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

What is state?
Data that can change while the app is running, and that the screen depends on - like the logged-in user or the cart.
Why did the header show "free" after the upgrade?
It had its own copy of the user. Only the profile page’s copy changed, and nothing told the header.
What are the three questions of state management?
Where does the data live? Who may change it, and how? How does everyone find out when it changes?
What does subscribe do in a store?
It registers a function that the store calls every time the state changes.
How do you change state in Redux?
By dispatching an action - store.dispatch(upgraded()). A reducer then produces the new state.

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. 1.

    Which of these is NOT state: the cart item count, the site logo, whether the menu is open, the search text?

  2. 2.

    Two components both call useState(userFromServer). One of them updates its user. What does the other one show?

  3. 3.

    What does the action object for upgraded() look like?

  4. 4.

    In an ES module, what happens when you write store.getState().user.plan = "enterprise" with Redux Toolkit?

  5. 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...