State management
Learn predictable JavaScript state with a single source of truth, pure reducers, immutable updates, tiny stores, and state machines.
- 01Pick an owner for each valueTell apart local UI state, shared app state, server cache state, URL state, and form state.
- 02Build a small storeUse
getState,subscribe, anddispatch, with a reducer that turns each action into the next state. - 03Stop changes that make no senseCopy instead of changing in place, work out values instead of storing them twice, and use a state machine to block steps that must not happen.
State is data that changes over time
A small program is easy to follow when all its data lives inside one function. A web app is harder. One click can change the header, a form, the web address, and a list of results all at once. State management means two things:
- You decide which one place owns each changing value.
- You decide the one way that value is allowed to change.
State is data that changes while the app runs and changes what the app shows or does. A cart's items are state. So is "is the menu open?". State management picks one owner for each piece of state, one way to change it, and a clear way for other code to read it.
State does not all live in one big shared place. Some state belongs to one part of the page. Some comes from the server. Some lives in the web address. Some is a form the user is still filling in. The skill is to pick the smallest owner that works. The next lesson, app architecture, shows how those owners fit into a whole app. This lesson stays with state itself.
The table below lists five kinds of state. Read each row as: "this kind of state is owned by this, for example these values."
| Kind of state | Who owns it? | Examples |
|---|---|---|
| Local UI state | One part of the page owns it. | Is the menu open? Which tab is picked? Is the tooltip showing? |
| Shared app state | One shared store owns it, because several parts read it. | The cart's items, the signed-in user, the chosen workspace. |
| Server cache state | The server owns it. The app keeps a copy. | Fetched products, profile data, pages of search results. |
| URL state | The web address owns it. | The search words, filters, sort order, and page number. |
| Form state | The form owns it until it is sent. | What the user typed, error messages, whether the form is sending. |
Imagine everyone at home kept their own shopping list. Two people might buy milk. Nobody might buy bread. With one shared list on the fridge, everyone sees the same answer.
- In real life: Everyone checks the same list
- In JavaScript: Every part of the page reads the same state
- In real life: One person writes on the list
- In JavaScript: One owner makes each change
- In real life: Nobody keeps a private list
- In JavaScript: No part of the page keeps its own copy to change
- In real life: The list shows what to buy now
- In JavaScript: The page shows the latest state
In code, that one shared place is called the single source of truth. Every part of the page reads from it, so they all agree.
Work out values instead of storing them twice
Here is a common bug. You store the cart's items in cart.items. You also store how many there are in cartItemCount. Then you remove an item and forget to change the count. Now the header says 3 and the cart shows 2.
The fix is simple. Store only the items. Work out the count and the total from the items when you need them. A value you work out from other state is called derived state.
const cart = [ { name: "Tea", price: 12, qty: 2 }, { name: "Mug", price: 5, qty: 1 },]; const itemCount = cart.reduce((total, item) => total + item.qty, 0);const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0); console.log(itemCount);console.log(total.toFixed(2));Line 1 stores the cart: an array of items. Only this array is stored.
- Line 6 works out the count.
reducewalks through the array and adds up eachitem.qty, starting from0. - Line 7 works out the total the same way: price times quantity for each item, added up.
- Lines 9 and 10 print
3and29.00. The count is 3 because two teas count as two.
If the cart changes, you run lines 6 and 7 again and get fresh numbers. There is no second value to keep in step.
const cart = [];
const itemCount = cart.reduce((sum, item) => sum + item.qty, 0);
console.log(itemCount);Line 2 starts reduce at 0. The cart has no items, so there is nothing to add. reduce just gives back its starting value, and line 4 prints 0.
A small function that reads or works out a value from state has a name: a selector. Working out a count like this is fast, so you can do it every time. Some apps save a result and reuse it while the inputs stay the same. That is called memoization. Only add it when the work is really slow, or when other code needs to get back the very same object each time.
const store = { cart: [{ name: "Tea", qty: 2 }] };
const headerCount = store.cart.reduce((sum, item) => sum + item.qty, 0);
const cartNames = store.cart.map((item) => item.name).join(", ");
console.log(headerCount);
console.log(cartNames);Line 1 makes one store object that holds the cart. Line 2 works out the count for the header. Line 3 works out the item names for the cart page. Both read the same store.cart. The code prints 2 and Tea.
store.cart. Neither keeps its own copy, so they can't disagree.A single source of truth
STEP THROUGHA store is an object that holds the state for the whole app. The store below is small. It has three methods:
getState()gives back the current state.subscribe(listener)saves a function to call after every change. That function is a subscriber.subscribegives back another function. Call it to unsubscribe, which means "stop calling me".dispatch(action)asks for a change. It is the only way to change the state.
An action is a plain object that says what happened, like { type: "cart/add", payload: { id: "tea" } }. The type names the change. The payload carries the data for it. A reducer is a function that takes the current state and an action, and gives back the next state. The next section looks at reducers closely.
Subscribers are like the observer pattern: they wait to be told about a change. The key point here is different: subscribers never keep their own copy of the cart. They read the store.
Follow one change from start to end: an action goes in, the reducer gives back a new state, and the subscriber runs. Then unsubscribe stops the subscriber.
script
let state = initialState; const listeners = new Set(); return { getState() { return state; }, subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); }, dispatch(action) { const previousState = state; state = reducer(state, action); if (previousState !== state) { listeners.forEach((listener) => listener(state, action, previousState)); } return action; }, };} function cartReducer(state, action) { switch (action.type) { case "cart/add": return { ...state, items: [...state.items, action.payload] }; default: return state; }} const store = createStore(cartReducer, { items: [] });const unsubscribe = store.subscribe((state, action) => { console.log(action.type + ": " + state.items.length);}); store.dispatch({ type: "cart/add", payload: { id: "tea", name: "Tea" } });unsubscribe();store.dispatch({ type: "cart/add", payload: { id: "mug", name: "Mug" } });console.log(store.getState().items.map((item) => item.id).join(", "));The step-through follows one change from start to end:
dispatch(action)hands the action to the store.- The reducer gives back the next state.
- The store keeps that next state in place of the old one.
- The store calls each subscriber, so each part of the page can show the new state.
dispatch hands the action to the reducer. The reducer returns the next state, the store keeps it, and then the subscriber runs and prints cart/add: 1.Unsubscribing matters too. When a part of the page goes away, it should stop listening. Line 39 does that. The second dispatch on line 40 still changes the store, but nothing prints for it.
This store does not lock its state. getState() gives back the very object the store holds. So other code could change it directly. The rule is: only read what getState() gives you, and ask for changes with dispatch. JavaScript does not check this rule for you.
Redux is a popular state library that made this shape well known. You don't need it to use the idea: one state value, plain action objects, reducers, and subscribers. Libraries add extra tools on top, such as browser tools that show every action.
Reducers and actions
RULES FOR CHANGESA reducer has this shape: (state, action) => nextState. It takes the current state and an action. It gives back the next state. A reducer must be a pure function. That means two rules:
- The same state and the same action always give the same result.
- It changes nothing outside itself. It doesn't fetch data, write to web storage, read the clock, or change the state it was given.
The pure functions lesson explains these rules in more detail.
Same state and same action in, same next state out, and nothing else touched. Work like fetching data or saving to storage happens before or after dispatch, never inside the reducer.
const addItem = (item) => ({ type: "cart/add", item });
function reducer(state, action) {
if (action.type === "cart/add") {
return { ...state, items: [...state.items, action.item] };
}
return state;
}
const before = { items: [] };
const after = reducer(before, addItem("tea"));
console.log(after.items.join(", "));
console.log(reducer(after, { type: "cart/unknown" }) === after);Line 1 is an action creator: a small function that builds an action object for you. addItem("tea") gives back { type: "cart/add", item: "tea" }.
- Lines 3 and 4 handle the add action. They give back a new state object with the item added.
- Line 6 handles every other action. It gives back the same state, unchanged.
- Line 11 prints
tea: the added item. - Line 12 sends an action the reducer doesn't know. It prints
true, because the reducer gave back the very same object. That tells the store nothing changed.
A real app has more than one area of state. You can give each area its own reducer. Each area is called a slice. For example:
- A cart reducer changes only the cart's items.
- A UI reducer changes only whether the cart panel is open.
- A root reducer calls both and puts their results together.
const addItem = (item) => ({ type: "cart/add", payload: item });const toggleCart = () => ({ type: "ui/toggle-cart" }); function cartReducer(state = { items: [] }, action) { if (action.type === "cart/add") { return { ...state, items: [...state.items, action.payload] }; } return state;} function uiReducer(state = { cartOpen: false }, action) { if (action.type === "ui/toggle-cart") { return { ...state, cartOpen: !state.cartOpen }; } return state;} function combineReducers(reducers) { return (state, action) => { let changed = false; const next = {}; for (const key of Object.keys(reducers)) { next[key] = reducers[key](state[key], action); changed = changed || next[key] !== state[key]; } return changed ? next : state; };} const reducer = combineReducers({ cart: cartReducer, ui: uiReducer });let state = { cart: { items: [] }, ui: { cartOpen: false } };state = reducer(state, addItem({ id: "tea" }));state = reducer(state, toggleCart());console.log(state.cart.items.length);console.log(state.ui.cartOpen);Lines 1 and 2 are action creators. Line 4 starts the cart reducer, and line 11 starts the UI reducer. Lines 18 to 28 build the root reducer:
- Line 23 calls each slice reducer with its own part of the state.
- Line 24 notes whether any slice gave back a new object.
- Line 26 gives back the new root object only if something changed. Otherwise it gives back the old one.
Lines 32 and 33 send two actions. The code prints 1 (one cart item) and true (the cart panel is open).
Copy instead of changing in place
SAME OBJECT OR NEW?A variable that holds an object really holds the object's address: where the object lives in memory. Two variables can hold the address of the same object. The objects and references lesson explains this.
State management uses this on purpose. A reducer never changes the old object. Instead it makes a new object with the change in it. This is called an immutable update. "Immutable" means "never changed". Changing an object in place is called mutating it.
Why bother? Because then one quick check tells you if anything changed: prevState !== nextState. A new object means a change. The same object means no change. Old states also stay as they were, so you can keep them for an undo button.
You only copy the parts that change. These tools make copies:
- Object spread,
{ ...obj }, from the spread and rest lesson. - Array spread,
[...arr], and the array methodsmapandfilter. toSorted()andwith(index, value). They give back a new array and leave the old one alone.
const board = { user: { name: "Asha", badges: ["member"] }, tasks: [ { id: "draft", title: "Draft reducer", done: false }, { id: "ship", title: "Ship lesson", done: false }, ], priorities: ["low", "medium", "high"],}; const nextBoard = { ...board, user: { ...board.user, badges: [...board.user.badges, "reviewer"] }, tasks: board.tasks.map((task) => task.id === "draft" ? { ...task, done: true } : task, ), priorities: board.priorities.with(1, "normal").toSorted(),}; console.log(board.tasks[0].done);console.log(nextBoard.tasks[0].done);console.log(board.tasks === nextBoard.tasks);console.log(nextBoard.priorities.join(", "));Line 10 starts a new board. Line 11 copies everything from the old board into it. Then:
- Line 12 makes a new
userwith a newbadgesarray that adds"reviewer". - Lines 13 to 15 make a new
tasksarray. Only the"draft"task is replaced, by a copy withdone: true. - Line 16 swaps
"medium"for"normal"withwith, then sorts the result.
Lines 19 to 22 print false, true, false, and high, low, normal. The old task is still not done, and the old board is untouched. For more ways to copy, see immutability.
const state = {
profile: { name: "Asha" },
settings: { theme: "light" },
};
const next = {
...state,
profile: { ...state.profile, name: "Mina" },
};
console.log(state.profile.name);
console.log(next.profile.name);
console.log(state.profile === next.profile);
console.log(state.settings === next.settings);Lines 5 to 7 copy two objects: the outer state and the profile inside it. The name changes only in the copy. Line 10 prints Asha and line 11 prints Mina.
Line 12 prints false: the two profiles are different objects. Line 13 prints true: settings did not change, so both states share it. A spread only copies one level. If you spread only the outer object, both states would share one profile. Changing the name would then change the old state too.
This reducer changes the old state instead of making a new one. Step through it and watch the subscriber miss a real change, because the state object stays the same.
script
function createStore(reducer, initialState) { let state = initialState; const listeners = new Set(); return { getState() { return state; }, subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); }, dispatch(action) { const previousState = state; state = reducer(state, action); if (previousState !== state) { listeners.forEach((listener) => listener(state, action)); } }, };} function badCartReducer(state, action) { if (action.type === "cart/add") { state.items.push(action.payload); return state; } return state;} const renders = [];store.subscribe((state) => { renders.push("rendered " + state.items.length);}); store.dispatch({ type: "cart/add", payload: { id: "tea" } });console.log(renders.length);console.log(store.getState().items.length);The step-through above shows the bug slowly. On line 25, push adds the item to the old array. The cart really changes. But the reducer gives back the same state object. So the store's check finds the same object, decides nothing changed, and skips the subscriber. The page would keep showing the old cart.
push changes the old cart, so the store thinks nothing changed and skips the subscriber. Right: the spread makes a new cart, so the store sees the change.const template = { user: { name: "Asha" }, created: new Date("2026-09-26T00:00:00Z"),}; const copy = structuredClone(template);copy.user.name = "Mina"; console.log(template.user.name);console.log(copy.user.name);console.log(Object.prototype.toString.call(copy.created));structuredClone(value) is a built-in function that makes a deep copy: it copies the object and every object inside it. Line 6 copies the template. Line 7 changes the copy's name. Line 9 prints Asha, so the original did not change. Line 10 prints Mina. Line 11 shows the date was copied as a real Date.
It is handy, but it is not how reducers usually copy:
- It throws an error for functions and page elements.
- Copying the whole state is much more work than copying the one part that changed.
- Parts that did not change become new objects too, so the quick "same object?" check stops working for them.
// Conceptual Immer-style code. Immer is not installed in this lesson.const nextState = produce(state, (draft) => { draft.cart.items.push({ id: "tea", name: "Tea" }); draft.ui.cartOpen = true;});Immer is a library with a produce function. You write code that seems to change a draft, a stand-in for the state. Immer watches those changes and builds a new state from them. The old state is never touched. Immer is not installed here, so this block only shows the idea and does not run.
State machines block steps that make no sense
FIXED STAGESReducers are good for changing data. Some state is different: it moves through a few named stages. A request to a server, for example, is always in one of four stages:
idle: nothing has happened yet.loading: the request was sent, and we are waiting.success: the answer came back.error: something went wrong.
It can never be loading and success at once. It also should not send a second request while the first one is still loading.
A state machine writes these rules down. It has a list of stages, called states. Things that happen, like SUBMIT, are called events. A transition table says which event moves which state to which next state. Any event not in the table for the current state is ignored.
SUBMIT while loading is not listed, so the machine stays in loading.A state machine has a list of states, events, and the moves between them. Step through an allowed event, an ignored one, and another allowed one.
script
idle: { SUBMIT: "loading" }, loading: { RESOLVE: "success", REJECT: "error" }, success: { RESET: "idle" }, error: { RETRY: "loading", RESET: "idle" },}; function transition(state, event) { const nextValue = transitions[state.value]?.[event.type]; if (!nextValue) return state; return { value: nextValue, context: { ...state.context, lastEvent: event.type }, };} let state = { value: "idle", context: { lastEvent: null } };for (const event of [{ type: "SUBMIT" }, { type: "SUBMIT" }, { type: "RESOLVE" }]) { const next = transition(state, event); console.log(state.value + " + " + event.type + " -> " + next.value); state = next;}The code that runs the machine is small. Line 9 looks up the event in the table, under the current state. The ?. there means "if this part is missing, give undefined instead of an error". If nothing is found, line 10 gives back the current state unchanged. That one line is what blocks the second SUBMIT.
Bigger apps sometimes need more: states inside states, or checks before a move. Libraries such as XState add these. But the core idea is this small table and lookup.
Where each piece of state should live
PLAYGROUNDThe first question is not "Which library should I use?". It is "Who owns this value?". Here are the usual answers:
- A value only one part of the page uses, like "is this tooltip showing?", stays in that part. This is local UI state.
- Data several parts of the app read, like the cart, goes in one shared place. This is shared app state. It does not mean a variable on
window. - Data fetched from a server belongs to the server. Your app keeps a copy, called a cache, with rules for when to fetch it again. This is server cache state.
- Choices someone might share or come back to, like the sort order or page number, go in the web address. This is URL state. The URL objects lesson covers how to read and change a URL.
- A form the user hasn't sent yet stays with the form.
isHelpPopoverOpeninside one button- Cart items shown in the header and on the checkout page
- A product list fetched from the server, with a loading flag and an age
?sort=price&page=2in the address bar- An email address typed into a sign-up form, not sent yet
- The signed-in user's name, shown in many places
- The saved answer from
/api/profile, with the time it was fetched - Search words that must survive a reload and work in a shared link
Put each card with its smallest owner that works. If someone should be able to share it, think URL. If the server owns it, think server cache.
const url = new URL("https://shop.example/products?sort=price&page=2");
const page = Number(url.searchParams.get("page"));
url.searchParams.set("page", String(page + 1));
console.log(page);
console.log(url.search);Line 1 makes a URL object from a product page address. Line 2 reads the page number, 2. Line 3 sets it to the next page. The code prints 2 and ?sort=price&page=3.
In a real app, the page should also follow the address. When the user presses Back or Forward, the page should show the page number in the address.
The playground below is a small todo list run by a reducer. Each button sends an action. The reducer gives back a new state, and the playground keeps every state in a list. The slider lets you go back to an older state. This is called time travel. It works because old states were never changed.
const initialState = { todos: [{ id: "draft", text: "Draft reducer", done: false }], filter: "all",}; function todoReducer(state, action) { switch (action.type) { case "todo/add": return { ...state, todos: [...state.todos, action.payload] }; case "todo/toggle": return { ...state, todos: state.todos.map((todo) => todo.id === action.payload.id ? { ...todo, done: !todo.done } : todo, ), }; case "filter/set": return { ...state, filter: action.payload }; default: return state; }} const selectOpenCount = (state) => state.todos.filter((todo) => !todo.done).length;let state = initialState;state = todoReducer(state, { type: "todo/add", payload: { id: "ship", text: "Ship lesson", done: false },});state = todoReducer(state, { type: "todo/toggle", payload: { id: "draft" } });console.log(selectOpenCount(state));- Draft reducer
open
@@init
This is the starting state. Press the buttons to send actions. Then drag the slider back to see older states.
Try it: add a todo, mark one done, then drag the slider back. The older state still shows the todo as open, because the reducer made a new state instead of changing the old one.
const preferences = { theme: "dark", sidebar: "open" };localStorage.setItem("cf-preferences", JSON.stringify(preferences)); const restored = JSON.parse(localStorage.getItem("cf-preferences") ?? "{}");const url = new URL("https://example.com/products?sort=price");url.searchParams.set("page", "2"); console.log(restored.theme);console.log(url.pathname + url.search);Saving state so it survives a page reload is called persistence. It is a place to save a copy, not a second owner.
- Lines 1 and 2 save private settings, like the theme, with
localStorage.setItem.JSON.stringifyturns the object into text, because storage only holds text. - Line 4 reads the text back and turns it into an object again with
JSON.parse. - Lines 5 and 6 put the page number in a URL, so someone can reload, bookmark, or share it.
- Lines 8 and 9 print
darkand/products?sort=price&page=2.
The code runner on this page can't use storage. Try this code in the console of a real web page.
Libraries give you ready-made tools. You still need to decide who owns each value. Some common ones:
- Redux Toolkit: a store with reducers, a record of every action, and Immer built in.
- Zustand: small stores with very little setup. A part of the page can listen to just the values it uses.
- Pinia: stores made for Vue apps.
- Signals: single values that tell their readers when they change. The observer pattern lesson covers them.
- XState: state machines with states, events, and checks, for flows where the order of steps matters.
Common misconceptions
- "All state belongs in one shared store." A toggle or an unsent form that one part of the page uses should stay with that part.
- "Storing the count makes the page faster." Store the items and work out the count. Only store it if you measure and find the work is really slow.
- "A reducer can change the old state if the page still looks right." Then the "same object?" check fails. Subscribers get skipped, and undo shows the wrong data.
- "State machines are only for huge apps." They help in any small flow with steps that must not happen, like sending a form twice.
- "Saving to localStorage is state management." Storage only keeps a copy. After the page loads it back, the app still needs one owner and one way to change it.
- "Signals mean I don't have to choose an owner." Signals change how readers are told about changes. You still decide what is local, shared, server cache, or URL state.
The table below compares four tools. Read each row as: "this tool is good for this, helps in this way, and needs care here."
| Tool | Good for | How it helps | What to watch for |
|---|---|---|---|
| A plain variable | One run of one function. | It is quick and simple. | It is gone when the function ends. Other parts can't read it. |
| One shared store | Data that several parts of the app read. | One way to change it, subscribers, a record of actions, and undo. | It fills up with clutter if every small toggle goes in it. |
| A state machine | Flows with a few named stages. | It ignores steps that must not happen, like sending twice while loading. | It is more setup than a simple counter or an open/closed toggle needs. |
| Signals | Single values that many readers watch. | A change tells only the readers of that value. | Each library has its own rules for how readers are tracked. |
Some nearby ideas have their own lessons. The observer pattern lesson covers subscribers and signals. The app architecture lesson covers how to split an app into parts. Structural and behavioral patterns covers commands and undo.
Practice exercises
5 EXERCISESRead the store code. Type the two printed values in order.
function createStore(reducer, initialState) {
let state = initialState;
const listeners = new Set();
return {
getState: () => state,
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
dispatch(action) {
state = reducer(state, action);
listeners.forEach((listener) => listener(state, action));
},
};
}
const reducer = (state, action) =>
action.type === "add" ? { ...state, count: state.count + action.payload } : state;
const store = createStore(reducer, { count: 0 });
store.subscribe((state) => console.log(state.count));
store.dispatch({ type: "add", payload: 2 });
store.dispatch({ type: "add", payload: 3 });The first dispatch changes count from 0 to 2. The second changes it from 2 to 5. So the subscriber prints 2, then 5.
Fill in the ui/toggle-cart case. Write a return that gives back a new object and leaves the old state unchanged.
function uiReducer(state, action) {
switch (action.type) {
case "ui/toggle-cart":
// Return the next state here.
default:
return state;
}
}return { ...state, cartOpen: !state.cartOpen };The return makes a new object. It copies everything from state and flips only cartOpen.
Which expression in the code shows that state and next are the same object?
const state = { items: [] };
const next = state;
next.items.push({ id: "tea" });
console.log(state === next);
console.log(state.items.length);const state = { items: [] };
const next = { ...state, items: [...state.items, { id: "tea" }] };
console.log(state === next);
console.log(state.items.length);
console.log(next.items.length);state === next prints true, so both names hold the same object. The fixed version makes a new outer object and a new items array.
This form machine stops a form from being sent twice. After two SUBMIT events, which state is it in?
const transitions = {
editing: { SUBMIT: "submitting" },
submitting: { RESOLVE: "success", REJECT: "error" },
error: { CHANGE: "editing", SUBMIT: "submitting" },
success: { RESET: "editing" },
};
function transition(state, event) {
return transitions[state]?.[event] ?? state;
}
let state = "editing";
state = transition(state, "SUBMIT");
state = transition(state, "SUBMIT");
console.log(state);After the first SUBMIT, the state is submitting. The submitting row has no SUBMIT, so the second one is ignored. The state stays submitting.
A teammate wants to store an itemCount value next to cart, and change it by hand. What should you store for the count instead?
const cart = [{ qty: 2 }, { qty: 3 }];
const itemCount = cart.reduce((total, item) => total + item.qty, 0);
console.log(itemCount);Store nothing extra. Work out the count from cart with reduce whenever you need it, so it can never disagree with the cart.
Check your understanding
8 QUESTIONSQuestion 1 of 8What does a single source of truth mean in an app?
Choose an answer to see the explanation.
Question 2 of 8What does this tiny store print?
Read the code, then predictfunction createStore(reducer, initialState) { let state = initialState; const listeners = new Set(); return { subscribe(listener) { listeners.add(listener); }, dispatch(action) { state = reducer(state, action); listeners.forEach((listener) => listener(state, action)); }, }; } const reducer = (state, action) => action.type === "cart/add" ? { ...state, items: [...state.items, action.payload] } : state; const store = createStore(reducer, { items: [] }); store.subscribe((state, action) => console.log(action.type + ": " + state.items.length)); store.dispatch({ type: "cart/add", payload: { id: "tea" } });Choose an answer to see the explanation.
Question 3 of 8What is wrong with this reducer?
Read the code, then predictfunction reducer(state, action) { if (action.type === "cart/add") { state.items.push(action.payload); return state; } return state; } const state = { items: [] }; const next = reducer(state, { type: "cart/add", payload: { id: "tea" } }); console.log(state === next); console.log(next.items.length);Choose an answer to see the explanation.
Question 4 of 8Which line leaves the original
itemsarray unchanged?Choose an answer to see the explanation.
Question 5 of 8What does this state machine print after the second SUBMIT?
Read the code, then predictconst transitions = { idle: { SUBMIT: "loading" }, loading: { RESOLVE: "success" }, }; const transition = (state, event) => transitions[state]?.[event] ?? state; let state = "idle"; state = transition(state, "SUBMIT"); state = transition(state, "SUBMIT"); console.log(state);Choose an answer to see the explanation.
Question 6 of 8Which value is derived state, so you should usually work it out instead of storing it?
Choose an answer to see the explanation.
Question 7 of 8Where should
?sort=price&page=2usually live?Choose an answer to see the explanation.
Question 8 of 8What do undo and time travel need from reducers?
Choose an answer to see the explanation.
Key takeaways
- State is data that changes while the app runs and changes what it shows or does. Not all of it belongs in one shared place.
- A single source of truth means one owner. Nobody keeps a second copy that could disagree.
- A reducer takes the current state and an action, and gives back the next state. It touches nothing else.
- Copying instead of changing in place makes the "same object?" check work, and keeps old states for undo.
- A state machine lists its stages and which events move between them. Any other event is ignored.
- Use
localStorageto save private settings. Use the URL for choices someone might share.
Remember the one-liner.
Give each changing value one owner, work out everything else from it, and change it only in one clear way.
Up next: Structuring an application. It shows how these state owners fit into the parts of a whole app.