Local State vs Global State
Most state belongs to one component. Learn to sort state into local and global with four simple questions, and see - with measured re-renders - what each wrong choice costs.
What you will be able to do
- Explain the difference between local state and global state, with examples
- Sort any piece of state with four questions: who reads it, who changes it, how far apart they are, and must it survive when the component disappears
- Know the four places state can live: the component, a parent, React Context, a store
- Measure the cost of making local state global: extra code, and extra re-renders from a whole-state selector
- See local state disappear when its component is removed, and fix it by lifting it up
- Build a small app that uses Redux for global state and useState for local state, side by side
The idea, in plain English
In Lesson 1 you saw two copies of the same user drift apart, and you fixed it by keeping one copy that everyone reads. A beginner often learns the wrong lesson from that: "so I should put all my state in one global store". This lesson shows why that is a mistake.
Local state is state that only one component needs. Whether a dropdown is open, what is typed in a search box, whether a card is showing its details - nobody else in the app cares. It belongs inside that component, with useState.
Global state is state that many parts of the app need, often far apart from each other. The logged-in user is shown in the header, checked on the settings page, and used to decide which lessons are unlocked. The cart count is shown in the header and changed by every "Add to cart" button. This state must live in one shared place.
Most state in a real app is local. The skill this lesson teaches is sorting: for each piece of state, deciding where it should live before you choose a tool. All code was run with React 19.3, Redux Toolkit 2.13.0 and react-redux 9.3.0, rendered in a test browser (jsdom) and clicked or typed into.
Worked example: Six pieces of state in one course app, split into two piles - and a search box measured both ways.
1 - "Is the header menu open?"
Only the header reads it and only the header’s Menu button changes it. Nobody else cares. It stops at the first question: local state.
Three pieces of state from a course app, each taken through the same questions.
Start at the top. Move down only when more components, further apart, need the data.
An everyday picture: your pocket notebook and the office notice board
At work you have a small notebook in your pocket. Your own to-do list is in it: "call the bank", "buy milk". Nobody else needs it, so it stays in your pocket. On the wall there is a notice board with the meeting room schedule. Everyone must see the same schedule, so it is on the board and not in anyone’s pocket.
Now imagine two mistakes. If you pin your shopping list on the notice board, the board fills with noise and nobody can find the meeting schedule. If someone keeps the meeting schedule only in their pocket, two teams book the same room. Each mistake has a cost - and the costs are different.
Local state is your pocket notebook. Global state is the notice board. Putting local state on the board makes the app noisy and complicated. Keeping global state in a pocket gives you the drift bug from Lesson 1.
Your pocket notebookLocal state: useState inside one component.The notice boardGlobal state: one shared place, like a Redux store.Shopping list on the boardLocal state made global: noise, extra code, harder to find things.Meeting schedule in a pocketGlobal state kept local: copies drift apart (Lesson 1).Six pieces of state, two piles
Here are six pieces of state from a course website like this one. Before reading the answer, decide for each one: local or global?
The test is simple: count who needs it. If one component reads it and changes it, it is local. If many components, in different parts of the page, read it or change it, it is global. Four of the six are local - and that is normal. In most apps, most state is local.
Is the header menu open?LOCAL. Only the header shows and toggles it.Is the mouse over this card?LOCAL. Only that one card changes its style.Text typed in the search boxLOCAL. Only the search box uses it (until results elsewhere need it - see the questions below).Is this card showing its details?LOCAL. Each card has its own.The logged-in userGLOBAL. Header, profile page, lesson pages and settings all need it.The items in the cartGLOBAL. The header badge, every "Add to cart" button and the checkout page.Four questions to decide
Counting components works most of the time. When you are unsure, ask these four questions. They are the same questions you will use for every piece of state in this course.
Notice that the fourth question can move state up even when only one component uses it. A comment draft that must survive switching tabs cannot live inside the tab that disappears - Example 3 shows this.
1. Who reads it?One component, a few, or many?2. Who changes it?Only the component that shows it, or other parts of the app too?3. How far apart are they?Siblings under one parent, or different pages and layouts?4. Must it survive?Should the value stay when the component is removed from the screen - a tab switch, closing a modal, changing page?Example 1: a search box with local state
Our test page has three components: a Header, a CourseList, and a SearchBox. The text in the search box is only used by the search box, so it lives there, in useState.
We typed "redux" - five key presses - and counted how many times each component rendered (ran its function). Only SearchBox rendered, once per key. Header and CourseList did not render at all, because nothing they use changed.
import { useState } from "react";
function Header() {
return <header>Ravi</header>;
}
function CourseList() {
return <ul><li>Python</li><li>Redux</li></ul>;
}
function SearchBox() {
const [text, setText] = useState(""); // local: only SearchBox uses it
return <input value={text} onChange={(e) => setText(e.target.value)} />;
}
export default function App() {
return (
<>
<Header />
<CourseList />
<SearchBox />
</>
);
}Header: 0
CourseList: 0
SearchBox: 5Example 2: the same text in a Redux store
Now the mistake: we put the search text in a Redux store, as if it were global. It needs a slice, an action, a Provider, useSelector and useDispatch - about three times as much code for the same input box. Every key press is now an action that goes through the store.
We also made one very common error on purpose. Header reads the whole state with useSelector((state) => state), and then uses only the user’s name. Typing "redux" now rendered Header five times, even though the name never changed. react-redux noticed and printed a warning in development: "Selector unknown returned the root state when called. This can lead to unnecessary rerenders."
CourseList selects only state.user.name, so it rendered 0 times. And when we fixed Header the same way, it also dropped to 0. So the honest lesson is: global state is not automatically slower. Its real costs are more code, more places to make mistakes, and data that lives in the store long after the search box is gone.
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";
const searchSlice = createSlice({
name: "search",
initialState: { text: "" },
reducers: {
typed(state, action) {
state.text = action.payload;
},
},
});
const userSlice = createSlice({ name: "user", initialState: { name: "Ravi" }, reducers: {} });
const store = configureStore({
reducer: { search: searchSlice.reducer, user: userSlice.reducer },
});
function Header() {
const state = useSelector((state) => state); // MISTAKE: the whole state
return <header>{state.user.name}</header>;
}
function CourseList() {
const name = useSelector((state) => state.user.name); // only what it needs
return <ul><li>Python</li><li>Redux</li></ul>;
}
function SearchBox() {
const text = useSelector((state) => state.search.text);
const dispatch = useDispatch();
return <input value={text} onChange={(e) => dispatch(searchSlice.actions.typed(e.target.value))} />;
}
export default function App() {
return (
<Provider store={store}>
<Header />
<CourseList />
<SearchBox />
</Provider>
);
} whole-state selector Header fixed to state.user.name
Header: 5 0
CourseList: 0 0
SearchBox: 5 5
react-redux warning (development only):
Selector unknown returned the root state when called. This can lead to unnecessary rerenders.
Selectors that return the entire state are almost certainly a mistake, as they will cause
a rerender whenever *anything* in state changes.Watch out: Never select the whole state with useSelector((state) => state). Select only the field the component shows. Module 7 (Selectors) covers this in depth.
Example 3: local state disappears with its component
Here is the case where the fourth question matters. A comment form has two tabs: Write and Preview. The draft lives in DraftBox, inside the Write tab. When you click Preview, React removes DraftBox from the screen, and its state is thrown away with it. We typed "Hello", clicked Preview, clicked Write again - and the box was empty.
The fix is not Redux. The draft only needs to outlive DraftBox, so we lift it one level up into the Tabs parent, which stays on the screen. Same steps, and the draft "Hello" was still there. Always try the smallest move first.
import { useState } from "react";
function DraftBox() {
const [draft, setDraft] = useState(""); // dies when DraftBox is removed
return <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />;
}
export default function Tabs() {
const [tab, setTab] = useState("write");
return (
<div>
<button onClick={() => setTab("write")}>Write</button>
<button onClick={() => setTab("preview")}>Preview</button>
{tab === "write" ? <DraftBox /> : <p>Preview</p>}
</div>
);
}import { useState } from "react";
function DraftBox({ draft, setDraft }) {
return <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />;
}
export default function Tabs() {
const [tab, setTab] = useState("write");
const [draft, setDraft] = useState(""); // lifted: Tabs stays on screen
return (
<div>
<button onClick={() => setTab("write")}>Write</button>
<button onClick={() => setTab("preview")}>Preview</button>
{tab === "write" ? <DraftBox draft={draft} setDraft={setDraft} /> : <p>Preview</p>}
</div>
);
}Tabs.jsx draft after switching tabs: "" <- lost
TabsLifted.jsx draft after switching tabs: "Hello" <- kept"Global" does not have to mean Redux
There are four places state can live, shown in the second diagram above. 1. Inside the component, with useState. 2. In a parent, passed down as props. 3. In React Context, which shares a value with every component below a provider without passing props by hand. 4. In a store like Redux, for the whole app, with strict rules about how it changes.
Each step down is more powerful and also more code. Start at the top and move down only when the questions tell you to. The comment draft stopped at step 2. A theme that dozens of components read may be fine at step 3. The cart, changed from many places with rules about what may change, is a good fit for step 4. Lesson 7 compares Context and Redux properly.
1. useState in the componentOne component. The default for everything.2. useState in a parent + propsA few components, close together. "Lifting state up" - Lesson 6.3. React ContextMany components below one provider, without passing props through every level.4. A Redux storeThe whole app; changes only through actions; every change can be traced.What each wrong choice costs
Both mistakes are common, and they hurt in different ways. Knowing the symptom helps you find the cause in a real codebase.
Local state made globalMuch more code for simple things; every key press becomes an action; old values stay in the store after the screen is gone; easy to cause extra re-renders with a wide selector.Global state kept localCopies drift apart and two screens disagree (Lesson 1); props passed through many components that do not use them (Lesson 5).State that must survive kept too lowIt is lost when the component disappears - like the draft in Example 3.Example 4: one app, both kinds of state
A real app uses both kinds together. In this small course shop, the cart and the user are global, in a Redux store, because the header and every course card need them. Whether the menu is open, and whether a card is showing its details, are local, in useState.
We clicked through it. Opening the menu changed only the header - the store did not change at all. Clicking "Add to cart" on the Redux card put it in the store, and the header badge went from 0 to 1. Opening the details on the Python card did not open them on the Redux card: each card has its own local state.
import { useState } from "react";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";
// ---- GLOBAL state: many parts of the app need it ----
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] },
reducers: {
added(state, action) {
state.items.push(action.payload);
},
},
});
const userSlice = createSlice({ name: "user", initialState: { name: "Ravi" }, reducers: {} });
export const store = configureStore({
reducer: { cart: cartSlice.reducer, user: userSlice.reducer },
});
function Header() {
const name = useSelector((state) => state.user.name);
const count = useSelector((state) => state.cart.items.length);
const [menuOpen, setMenuOpen] = useState(false); // LOCAL: only the header cares
return (
<header>
{name} | Cart: {count}
<button onClick={() => setMenuOpen(!menuOpen)}>Menu</button>
{menuOpen && <nav>Courses · Blog · Logout</nav>}
</header>
);
}
function CourseCard({ title }) {
const dispatch = useDispatch();
const [showDetails, setShowDetails] = useState(false); // LOCAL: one card only
return (
<article>
<h3>{title}</h3>
<button onClick={() => setShowDetails(!showDetails)}>Details</button>
{showDetails && <p>12 lessons</p>}
<button onClick={() => dispatch(cartSlice.actions.added(title))}>Add to cart</button>
</article>
);
}
export default function App() {
return (
<Provider store={store}>
<Header />
<CourseCard title="Python" />
<CourseCard title="Redux" />
</Provider>
);
}start header: "Ravi | Cart: 0"
click Menu header shows the nav store: {"cart":{"items":[]},"user":{"name":"Ravi"}} <- unchanged
Add to cart (Redux) header: "Ravi | Cart: 1" store.cart: {"items":["Redux"]}
Details (Python card) Python card shows "12 lessons", Redux card does notThe rule to remember: start local
Keep state as close as possible to the components that use it. This is often called colocation. When you write a new component, start with useState. Move the state up - to a parent, to context, to a store - only when one of the four questions says you must.
Moving state up later is normal and not difficult: you saw it in Example 3. Moving state down out of a big global store is much harder, because by then many files depend on it. So when you are unsure, start local.
Common questions
Is React Context global state? It can be. Context shares a value with every component below its provider. If the provider is at the top of the app, the value is available everywhere. Lesson 7 explains why Context is a way to pass values, not a full state manager.
Should form state ever be global? Usually not. But a long multi-step form whose answers must survive moving between pages may need to live higher - in a parent route, or sometimes in a store. Use the four questions.
Does global state survive a page reload? No. Global state lives in memory, like local state. When the page reloads, the store starts again from its initial state. Keeping data across reloads (localStorage, the server) is a different topic, covered later in the course.
As a beginner, is it safer to put everything in Redux? No. It makes simple things complicated and hides which state matters. Use Redux for state that really is shared - this course will show you how to recognise it.
This lesson at a glance
Local stateUsed by one component.
const [open, setOpen] = useState(false)
Lifted stateKept in a parent, passed down as props.
<DraftBox draft={draft} setDraft={setDraft} />Global stateUsed by many components, far apart.
configureStore({ reducer: { cart } })Read global stateSelect only what you show.
useSelector((state) => state.user.name)
Change global stateDispatch an action.
dispatch(cartSlice.actions.added(title))
AvoidSelecting the whole state.
useSelector((state) => state)
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Open an app you use every day. Find eight pieces of state on one screen and put each in the local or the global pile.”
“Add console.log("Header render") to Header in GlobalSearch.jsx. Type in the box, then change the selector to state.user.name and type again.”
“Run Tabs.jsx, type something, switch tabs and back. Then try TabsLifted.jsx.”
“In CourseApp.jsx, open Details on one card. Why does the other card stay closed?”
What usually goes wrong
A dropdown, a hover, a search box only one component uses - these are local. Global versions mean more code and more mistakes.
✗ dispatch(ui.actions.menuToggled()) // only the header cares✓ const [menuOpen, setMenuOpen] = useState(false);The component re-renders when anything in the store changes. Select only the field you show.
✗ const state = useSelector((state) => state);✓ const name = useSelector((state) => state.user.name);Two copies drift apart (Lesson 1). Data that many components use needs one owner.
✗ function Header() { const [cart] = useState([]); ... }
function Checkout() { const [cart] = useState([]); ... }✓ const count = useSelector((state) => state.cart.items.length);The comment draft only needed to move one level up. Try the parent before context or a store.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Sort these eight pieces of state into local or global: (1) the selected tab on a lesson page, (2) the logged-in user, (3) whether a password is shown or hidden, (4) the theme, (5) the number of unread notifications, (6) text in a comment box, (7) the saved-lessons list, (8) whether a tooltip is visible.
Show hintHide hint
For each one, ask: which components read it, and are they far apart?
Show solutionHide solution
(1) selected tab local - only that page's tab bar
(2) logged-in user global - header, pages, settings
(3) show/hide password local - only the password field
(4) theme global - every component's colours
(5) unread notifications global - header bell + notifications page
(6) comment box text local - or lifted, if a preview needs it
(7) saved-lessons list global - header menu + every Save button
(8) tooltip visible local - only that tooltipFix the Header in GlobalSearch.jsx so that typing in the search box no longer re-renders it.
Show hintHide hint
Header only shows the user’s name. What should useSelector return?
Show solutionHide solution
function Header() {
const name = useSelector((state) => state.user.name); // only the name
return <header>{name}</header>;
}
// Measured: typing "redux" now renders Header 0 times (it was 5).In Tabs.jsx the draft is lost when you switch tabs. Without Redux, what is the smallest change that keeps it?
Show hintHide hint
Which component stays on the screen when you switch tabs?
Show solutionHide solution
Move useState("") for the draft from DraftBox up into Tabs,
and pass draft and setDraft down as props (TabsLifted.jsx).
Tabs is never removed, so the draft survives the tab switch.A quantity picker and a cart badge
Build a small product row. The number chosen with + and - is local until the user clicks Add; only then does it go into the global cart, and the cart badge updates.
- The cart items live in a Redux store: [{ title, quantity }]
- The chosen quantity lives in useState inside the picker, starting at 1, never below 1
- Pressing + or - must not change the store
- The badge shows the total quantity in the cart, with a narrow selector
Show one solutionHide solution
import { useState } from "react";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] }, // [{ title, quantity }]
reducers: {
added(state, action) {
state.items.push(action.payload);
},
},
});
export const store = configureStore({ reducer: { cart: cartSlice.reducer } });
function CartBadge() {
// a narrow selector: only re-renders when the total changes
const total = useSelector((state) =>
state.cart.items.reduce((sum, item) => sum + item.quantity, 0),
);
return <span>Cart: {total}</span>;
}
function QuantityPicker({ title }) {
const [quantity, setQuantity] = useState(1); // local until "Add"
const dispatch = useDispatch();
return (
<div>
{title}
<button onClick={() => setQuantity(Math.max(1, quantity - 1))}>-</button>
<span>{quantity}</span>
<button onClick={() => setQuantity(quantity + 1)}>+</button>
<button onClick={() => dispatch(cartSlice.actions.added({ title, quantity }))}>
Add
</button>
</div>
);
}
export default function App() {
return (
<Provider store={store}>
<CartBadge />
<QuantityPicker title="Notebook" />
</Provider>
);
}Key points
- Local state is used by one component; global state is used by many components, often far apart.
- Most state in a real app is local - start with useState.
- Four questions: who reads it, who changes it, how far apart are they, must it survive when the component disappears?
- State can live in four places: the component, a parent, React Context, a store. Move down only when needed.
- Local state is destroyed when its component is removed - lift it to a parent that stays.
- Global state is not automatically slower, but it costs more code, and a whole-state selector causes extra re-renders.
- Select only what a component shows: useSelector((state) => state.user.name).
- Moving state up later is easy; moving it down out of a store is hard. When unsure, start local.
Quick check before you move on
Interview questions
How do you decide whether state should be local or global?
By who reads it and who changes it, how far apart those components are, and whether the value must outlive the component. One component: local. A few nearby: lift to the common parent. Many, far apart, with shared rules for changes: global, via context or a store.
What is state colocation, and why does it matter?
Keeping state as close as possible to where it is used. It keeps components self-contained, reduces code and re-renders, and makes it obvious who owns the data. State can be moved up later when needed.
What goes wrong when you put all state in Redux?
Simple UI state needs actions, reducers and selectors; the store fills with values that matter to one component; stale values stay after components unmount; and wide selectors cause unnecessary re-renders.
Why should a useSelector not return the whole state?
useSelector re-renders the component when the selected value changes. The whole state changes on every action, so the component re-renders on every action. react-redux warns about this in development.
Is React Context a replacement for Redux?
Context passes a value down the tree without props; it does not add rules for how state changes, devtools, or fine-grained subscriptions. It suits values that change rarely, like a theme. Redux suits frequently changing, widely shared state that needs traceable updates.
Quiz
- 1.
Typing 5 keys into a search box with local state: how many times did Header render?
- 2.
With the text in Redux and Header using useSelector((state) => state), how many times did Header render while typing 5 keys?
- 3.
The comment draft is lost when you switch tabs. Is Redux the right fix?
- 4.
Name the four places state can live, from closest to widest.
- 5.
Six pieces of state: menu open, hover, search text, card details, logged-in user, cart. Which are global?
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