← Back to Redux
Lesson 2 · State Management Fundamentals

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.

Beginner35 min

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.

workflowWhere should this state live?step 1 / 3

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.

read by
Header
changed by
Header (Menu button)
lives in
useState in Header
in Redux?
no

Three pieces of state from a course app, each taken through the same questions.

ArchitectureFour places state can livetap a node to trace it

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.

Notebook and notice board -> state
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.

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

Ask about each piece of state
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.

LocalSearch.jsx
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 /> </> ); }
Renders while typing "redux" (5 keys) - measured
Header: 0 CourseList: 0 SearchBox: 5

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

GlobalSearch.jsx
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> ); }
Renders while typing "redux" (5 keys) - measured
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.

Tabs.jsx - the draft is lost
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> ); }
TabsLifted.jsx - the parent keeps the draft
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> ); }
Type "Hello", click Preview, click Write - measured
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.

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

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

CourseApp.jsx
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> ); }
Clicking through it - measured
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 not

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

Used by one component.

const [open, setOpen] = useState(false)
Lifted state

Kept in a parent, passed down as props.

<DraftBox draft={draft} setDraft={setDraft} />
Global state

Used by many components, far apart.

configureStore({ reducer: { cart } })
Read global state

Select only what you show.

useSelector((state) => state.user.name)
Change global state

Dispatch an action.

dispatch(cartSlice.actions.added(title))
Avoid

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

Sort your app

“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.”

Count renders

“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.”

Lose the draft

“Run Tabs.jsx, type something, switch tabs and back. Then try TabsLifted.jsx.”

Local per card

“In CourseApp.jsx, open Details on one card. Why does the other card stay closed?”

What usually goes wrong

Putting everything in the store

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);
Selecting the whole state

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);
Keeping shared data in two components

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);
Jumping straight to Redux when lifting is enough

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.

1.

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 hint

For each one, ask: which components read it, and are they far apart?

Show solution
Answer
(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 tooltip
2.

Fix the Header in GlobalSearch.jsx so that typing in the search box no longer re-renders it.

Show hint

Header only shows the user’s name. What should useSelector return?

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

In Tabs.jsx the draft is lost when you switch tabs. Without Redux, what is the smallest change that keeps it?

Show hint

Which component stays on the screen when you switch tabs?

Show solution
Answer
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.
Coding challenge

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.

It should
  • 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 solution
Solution - checked: + + gives picker 3 and Cart 0; Add gives Cart 3
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

What is local state?
State that only one component needs - it lives inside that component, usually with useState.
What is global state?
State that many parts of the app need, often far apart - like the logged-in user or the cart.
Is "is the dropdown open?" local or global?
Local. Only the dropdown reads it and changes it.
What happens to a component’s useState when the component is removed from the screen?
It is thrown away. When the component appears again, it starts from the initial value.
Where should you start when you add new state?
Local, in the component that uses it. Move it up only when the four questions say you must.

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

    Typing 5 keys into a search box with local state: how many times did Header render?

  2. 2.

    With the text in Redux and Header using useSelector((state) => state), how many times did Header render while typing 5 keys?

  3. 3.

    The comment draft is lost when you switch tabs. Is Redux the right fix?

  4. 4.

    Name the four places state can live, from closest to widest.

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