The previous module ended with an uncomfortable diagnosis: CicloUrbano's state is scattered across the whole application — the session in UserContext, the theme in ThemeContext, notices in NoticesContext, bookings in a reducer inside BookingsContext, the catalogue filter in the URL, and the search term in a useState inside CataloguePage — and nobody could say for sure where the next piece of data that shows up should go. The temptation at this point is to pick a library and trust it to sort out the mess. That's exactly the wrong decision, and in the wrong order. State gets classified first, the tool gets chosen second, because half the problems people try to solve with Redux disappear simply by putting each piece of data where it belongs. In this lesson you'll build a taxonomy of state, apply it to CicloUrbano's actual inventory, learn a decision tree that works for any new piece of data, understand the two symmetric mistakes — over-lifting and globalizing too soon — recover the distinction between state and derived value, and map out the options available. By the end you'll have a written audit of CicloUrbano and the plan for what each lesson in the module reorders.
Contents
- What "managing state" actually means
- The taxonomy: six types of state
- A type-by-type walk through CicloUrbano's inventory
- The decision tree
- The two symmetric mistakes: over-lifting and premature globalization
- Single source of truth and derived state
- The landscape of options: what exists and when you'd pick it
- Auditing CicloUrbano's current state
- The module's plan
- What "managing state" actually means
So far you've learned mechanisms: useState holds a value between renders, useReducer concentrates transitions, useContext avoids passing props through ten levels, useSearchParams puts a value in the URL. Managing state isn't learning one more mechanism. It's answering, for every piece of data in the application, three questions:
- Who owns it? That is, the single place where that data genuinely exists. Everything else should read it from there, not copy it.
- Who needs it? The set of components that read it or change it. That set determines how far up it has to go.
- When does it expire? A local value lives as long as the component does. Server data can go stale a second after it arrives. They're not the same kind of thing.
When these three questions get answered wrong, the familiar symptoms show up: two parts of the screen showing different figures for the same thing, a form that empties itself on navigation, a counter that resets for no reason, a theme change that repaints the entire catalogue, a useEffect syncing one piece of state with another. None of those symptoms gets cured by installing a package.
State management is, above all, a problem of placement. Choosing a library is the last decision, not the first.
- The taxonomy: six types of state
This is the lesson's central table. It's worth reading slowly, because the rest of the module leans on it.
| Type | What it is | Example in CicloUrbano | Where it should live | Tool | If you put it in the wrong place |
|---|---|---|---|---|---|
| Local UI state | A visual detail that only affects the component that renders it | A dropdown being open, a panel's active tab, whether UserMenu is expanded |
Inside the component itself | useState, useToggle |
Lifted: the parent re-renders half the screen just to open a menu |
| Shared among a few | Data that two or three sibling components coordinate around | The selected bike in the catalogue, highlighted by BikeList and shown by BookingPanel |
In the nearest common ancestor | Lifting state up (04-01) + props | Global: it ends up in a store where it doesn't belong, and nobody knows who's changing it |
| Application-wide global | Data read by half the application and that changes rarely | Session (user, isOperator), visual theme, notices |
Near the root, accessible without props | Context, Redux Toolkit, Zustand | Passed through props: six levels of prop drilling with props that exist only to pass through |
| Server state | A local copy of data that lives in a remote database | Bikes, stations, bookings, users | In a cache with its own lifecycle | TanStack Query, RTK Query, the router's loader |
In useState or in Redux: you end up hand-writing loading, error, cancellation, revalidation, and invalidation |
| URL state | Data that must be shareable by link or survive a reload | The catalogue filter (?tipo=electrica), the detail page's bicicletaId |
In the address itself | useSearchParams, useParams (06-02) |
In useState: the link you send a colleague won't show what you're seeing |
| Form state | The values the user is typing, plus their validation | The draft in BookingForm |
Next to the form, or in its own reducer | Controlled useState, useReducer, React Hook Form |
Global: every keystroke dispatches an action and re-renders everything that reads the store |
Notice a detail that turns out to be decisive in 07-06: server state isn't a variant of global state, it's a separate category. It shares with it the fact that half the application reads it, but it differs on the essentials: you don't own it, it goes stale on its own, and someone else can change it while you're looking at it.
- A type-by-type walk through CicloUrbano's inventory
The table is abstract until it's applied. Let's go data point by data point through what's already written in the project.
3.1 Local UI state: UserMenu's dropdown
// src/components/UserMenu.jsx
function UserMenu() {
const [open, toggle] = useToggle(false); // hook from 05-06
const { user, signOut } = useUser();
// …
}open is the canonical example of local state: nobody outside UserMenu needs to know about it, it doesn't need to survive a navigation, and there's no sense in sharing it by link. If you lifted it to Layout, opening the menu would trigger a render of Layout and, with it, of Header, Breadcrumbs, and the entire <Outlet />. An enormous cost for nothing in return.
Rule: if writing the state inside the component doesn't bother anyone, leave it there. State moves up when someone needs it, not just in case.
3.2 Shared among a few: the selected bike
In the catalogue, BikeList visually marks the chosen card and BookingPanel shows its data. They're siblings, so the data lives in their common ancestor, CataloguePage, and flows down through props. It's exactly what you did in 04-01.
// src/pages/CataloguePage.jsx (excerpt)
const [selectedId, setSelectedId] = useState(null);
// …
<BikeList bikes={visible} selectedId={selectedId} onSelect={setSelectedId} />
<BookingPanel bike={visible.find((bike) => bike.id === selectedId)} />Two levels of props aren't a problem. Prop drilling starts to hurt once you're three or four levels deep in components that receive a prop only to pass it along, without using it.
3.3 Application-wide global: session, theme, and notices
All three fit the same profile: lots of people read them, they're far apart in the tree, and they change rarely.
| Data | Who reads it | How often it changes |
|---|---|---|
user, isOperator |
Header, UserMenu, ProtectedRoute, RequireRole, WorkshopPage, BookingForm |
Very low: on sign-in and sign-out |
| Visual theme | ThemeButton and the document's data-theme attribute |
Very low: whenever the user clicks it |
| Notices | NoticeList renders them; anyone can trigger one |
Medium: on every operation that reports something |
The fact that the change frequency is low is what makes context a reasonable choice for the first two. With notices, nuances already start to appear, and that nuance is the subject of 07-02.
3.4 Server state: bikes, stations, bookings, and users
Today, in CicloUrbano, they live in src/data/domain.js as imported constants. That's a teaching simplification that has held up for six modules, but it's a lie: in a real application, bici-002 is rented because someone rented it three minutes ago from another device, and your local copy has no idea.
// src/data/domain.js — the shortcut we've used up to now
export const bikes = [
{ id: 'bici-001', model: 'Classic Urban', type: 'urbana', status: 'disponible', stationId: 'est-01', pricePerHour: 2.5 },
{ id: 'bici-002', model: 'Electric Pro', type: 'electrica', status: 'alquilada', stationId: 'est-01', pricePerHour: 4.0 },
// …
];In 07-06 this becomes a fictitious API served with json-server, and with it comes the whole list of problems that justifies a dedicated library. For now it's enough to flag it in the inventory: these four collections are not client state.
3.5 URL state: the catalogue filter
You already solved this in 06-02: ?tipo=electrica lives in the address, gets read with useSearchParams, and chosenType doesn't exist as a useState. The proof that the decision was right is that you can copy the address bar, paste it to a colleague, and both of you see the same thing. That's the criterion: if the data needs to travel in a link, it goes in the URL, and nowhere else.
An important warning for the rest of the module: once Redux arrives, there'll be a very clear temptation to move the filter into the store "to have it all together." Don't. It would duplicate the source of truth, and you'd have to sync the URL and the store with an effect — exactly what useSearchParams avoids.
3.6 Form state: the booking draft
The draft in BookingForm is ephemeral, changes on every keystroke, and only matters while the form is open. It lives in bookingsReducer because it shares transitions with submission (submit_started, booking_created, submit_failed), not because it's global. It's a textbook case of why classification matters: the draft lives in a context, but it isn't global state.
- The decision tree
Whenever a new piece of data shows up, walk this from top to bottom and stop at the first "yes."
flowchart TD
A["New piece of data"] --> B{"Does it come from a server?"}
B -- Yes --> C["Server-state cache<br/>TanStack Query · 07-06"]
B -- No --> D{"Must it be shareable<br/>by link or survive<br/>a reload?"}
D -- Yes --> E["URL<br/>useSearchParams / useParams"]
D -- No --> F{"Does only one<br/>component use it?"}
F -- Yes --> G["Local useState"]
F -- No --> H{"A few nearby<br/>siblings?"}
H -- Yes --> I["Lift to the common ancestor<br/>+ props"]
H -- No --> J{"Does half the app read it<br/>and does it change rarely?"}
J -- Yes --> K["Context · 07-02"]
J -- No --> L{"Does it change often,<br/>are there many consumers,<br/>or is the logic complex?"}
L -- Yes --> M["Dedicated store<br/>Redux Toolkit · 07-03…07-05"]
L -- No --> K
Two observations about the tree:
- The first question is the server one, and that's deliberate. It's the one most people skip, and the one that saves the most work when you get it right. Putting remote data into a client store is the most expensive architecture mistake in this module.
- The Redux branch comes last. Not because Redux is bad, but because reaching it means you've already ruled out five cheaper alternatives. When you reach Redux having ruled out those five, Redux is an excellent decision.
- The two symmetric mistakes: over-lifting and premature globalization
Beginners put state too low and suffer for it; intermediates overcorrect and put it too high. Both extremes have recognizable symptoms.
5.1 Over-lifting
It consists of lifting a piece of state higher than it needs to be, "in case someone else needs it someday."
Concrete symptoms:
- Opening a dropdown re-renders the header, the breadcrumbs, and the entire page.
- Components that receive five props and use one: the other four just pass through.
- A parent with nine
useStatecalls that have nothing to do with each other. - Changing a leaf component forces you to touch three files above it.
// ANTI-PATTERN: the state of a visual detail, lifted with no need
function Layout() {
const [menuOpen, setMenuOpen] = useState(false); // ← only UserMenu uses this
return (
<div>
<Header menuOpen={menuOpen} onToggleMenu={setMenuOpen} />
<Outlet />
</div>
);
}Header doesn't use menuOpen: it only carries it down to UserMenu. And every click on the menu triggers a render of Layout — that is, of the entire application.
5.2 Premature globalization
It consists of installing a global-state library before you have a problem that justifies it.
Concrete symptoms:
- A store with
menuOpen,modalVisible, andactiveTab: local state disguised as global. - Actions dispatched by only one component and read by that same component.
- Five files touched (action, reducer, selector, component, test) to add a checkbox.
- Nobody can remove a field from the store because nobody knows who reads it.
| Over-lifting | Premature globalization | |
|---|---|---|
| What goes wrong | State climbs higher than necessary | Everything ends up in a single store |
| Main cost | Unnecessary renders and pass-through props | Complexity, indirection, and ceremony |
| How to spot it | Unused props, cascading renders | Local state inside the store |
| How to fix it | Push state back down to the component that uses it | Pull out of the store whatever isn't global |
The fix for both is the same sentence: state should live at the lowest point that's still common to all of its consumers. No higher, no lower.
- Single source of truth and derived state
Here we pick back up, with bigger consequences, something you already saw in 05-01: don't store as state anything you can compute from other state.
// ANTI-PATTERN: duplicated state synced with an effect
const [bikes, setBikes] = useState(initialData);
const [type, setType] = useState('todos');
const [visible, setVisible] = useState(initialData); // ← a derived value stored as state
useEffect(() => {
setVisible(bikes.filter((bike) => type === 'todos' || bike.type === type));
}, [bikes, type]);Three chained problems: there's one extra render (React paints with a stale visible and repaints after the effect runs), there's a window where visible and type contradict each other, and anyone who changes bikes in the future without going through the effect breaks the consistency.
// CORRECT: the derived value is computed during render
const [bikes, setBikes] = useState(initialData);
const type = params.get('tipo') ?? 'todos'; // the URL is the source of truth
const visible = bikes.filter((bike) => type === 'todos' || bike.type === type);visible stops existing as state. It can't drift out of sync, because it's recomputed on every render from the only two sources of truth. If that calculation ever turns out to be expensive, the answer is to memoize it with useMemo (08-03), not turn it into state.
Cases of derived state that show up in CicloUrbano and shouldn't be stored:
| Value | Computed from | Never store it because |
|---|---|---|
isOperator |
user.role === 'operario' |
It drifts out of sync when the session changes |
visible |
bikes + the URL filter + the search term |
It drifts out of sync on every filter change |
canSubmit |
Validation errors + submitState |
It drifts out of sync while typing in the form |
bookingTotal |
pricePerHour × hours |
It drifts out of sync when the hours change |
freeDocks |
station.docks − bikes at the station |
It drifts out of sync when a bike moves |
Rule: if you can write the value as an expression of other values, it's derived. Write it as an expression.
- The landscape of options: what exists and when you'd pick it
This table is the map of the terrain. It isn't a single recommendation: the column that matters is "when you'd pick it."
| Option | What it solves | When you'd pick it | Entry cost |
|---|---|---|---|
| Props + lifting state | Coordinating a few nearby components | Whenever the distance is one to three levels | None: it's already in React |
| Context | Avoiding pass-through props for ambient data | Session, theme, locale, notices: few changes, many readers | Low, but with a performance cost if the value changes often (07-02) |
useReducer + context |
Complex, shared transition logic | A domain with many actions and a wide tree of consumers | Low: no new dependencies |
| Redux Toolkit | A single store, traceable actions, debugging tools | Large applications, multi-person teams, state logic that needs auditing | Medium: one dependency, a vocabulary, and a folder structure |
| Zustand | A minimal global store with granular selection, no provider | You want global state without Redux's ceremony and don't need time travel | Very low: one file and one hook |
| Jotai | Atomic state: small units that combine, re-rendering only whoever reads them | Many independent pieces of state with dependencies between them | Low, but it requires thinking in atoms, not objects |
| TanStack Query | Server state: cache, loading, error, revalidation, invalidation | As soon as the application reads data from an API. Almost always | Medium, and it pays for itself on the first screen |
On Zustand and Jotai, just enough to place them, with no tutorial, because this module teaches Redux Toolkit:
- Zustand defines a store as a function that returns state and actions, and components subscribe with a selector:
const user = useStore((s) => s.user). There's no provider, no actions with atype, and only whoever reads what changed gets re-rendered. It's the usual alternative when Redux feels like too much ceremony. - Jotai flips the model: instead of one big object, it defines small atoms (
const themeAtom = atom('light')) that get read withuseAtom. Derived atoms recompute themselves, and only whoever depends on the atom that changed gets re-rendered.
Both are legitimate options, and neither replaces TanStack Query for remote data. The most common choice today in a mid-sized application isn't "Redux or context," but "one tool for client state and another for server state."
- Auditing CicloUrbano's current state
With the taxonomy in hand, here's a snapshot of the application at the end of module 6, and the verdict on each piece.
| Data | Where it lives today | Actual type | Verdict | Lesson that handles it |
|---|---|---|---|---|
user, isOperator, loadingSession |
UserContext |
Global | Correct; the value needs to be stabilised | 07-02, later sessionSlice in 07-04 |
| Visual theme | ThemeContext |
Global | Correct, and it stays that way for the whole module | 07-02 |
notices, showNotice |
NoticesContext |
Global | Correct, but state needs to be separated from actions | 07-02 |
bookings, draft, submitState, error |
BookingsContext with useReducer |
Mixed: the list is server state, the draft is form state | To be split | 07-04 and 07-06 |
Filter ?tipo= |
URL | URL state | Correct; not moved to Redux | Stays as is |
Search term |
useState in CataloguePage |
Local today, shared as soon as another screen reads it | Worth revisiting | 07-04 (catalogueSlice) |
bikes, stations |
Constants in src/data/domain.js |
Server state | To be moved to a query cache | 07-06 |
selectedId |
useState in CataloguePage |
Shared among a few | Correct: stays where it is | Stays as is |
open in UserMenu, modals |
Local useState / useToggle |
Local UI state | Correct: never enters the store | Stays as is |
Two conclusions from the audit worth underlining before moving on:
- Most of what's there is already in the right place. This module isn't going to throw away the work from modules 5 and 6: it's going to reorder three things and extract a fourth.
- The most serious problem isn't context — it's that domain data is being treated as client state. That's what 07-06 fixes, and it's the highest-impact change in the whole module.
- The module's plan
flowchart LR
A["07-01<br/>Classify"] --> B["07-02<br/>Context done right"]
B --> C["07-03<br/>Redux store"]
C --> D["07-04<br/>Slices and selectors"]
D --> E["07-05<br/>Connecting to React"]
E --> F["07-06<br/>Server state"]
| Lesson | What it reorders in CicloUrbano |
|---|---|
| 07-02 Context API | Splits BookingsContext into state and actions, composes the four providers into a single Providers, and establishes where context stops paying off |
| 07-03 Redux Introduction | Creates src/store/store.js with configureStore and provides it in main.jsx, with DevTools working |
| 07-04 Actions and Reducers | Writes bookingsSlice, catalogueSlice, and sessionSlice with their selectors and their async loading |
| 07-05 Connecting to React | Rewrites CataloguePage, BookingsPage, BookingsPanel, and UserMenu with useSelector and useDispatch, and decides what stays out |
| 07-06 Server State | Spins up the API with json-server, replaces useFetchBikes with useBikes built on TanStack Query, and settles the final architecture |
Common Mistakes and Tips
Mistake 1: choosing the library before classifying the state. "Let's use Redux" isn't an architecture decision, it's a postponement. Classify first; you'll often discover that half of your "global state" was server state and the other half was local state.
Mistake 2: treating server data as ordinary state. It's the most expensive and most frequent mistake. If the data has an id that exists in a database, it isn't yours: it's a copy that expires.
Mistake 3: storing derived values. Every useEffect whose only job is to setSomething(...) from other state is a badly stored derived value. Delete it and compute the expression during render.
Mistake 4: moving to a global store what already lives in the URL. You end up with two sources of truth and an effect syncing them. The URL always wins for whatever needs to be shareable by link.
Mistake 5: confusing "many components use it" with "it changes a lot." These are different axes that determine different things: the first decides where it lives, the second decides which tool to use. Data read by twenty components that changes once per session is context's ideal case; data read by twenty components that changes on every keystroke is the worst case.
Tip 1: write the inventory down. A three-column table — data, type, where it lives — in the project's README avoids months of arguments. When someone adds a new piece of data, the table tells them where to put it.
Tip 2: always start with local useState. Lifting a piece of state later is a ten-minute refactor. Pulling it back down from a global store that eight components have already hooked into is an afternoon-long refactor.
Tip 3: measure before optimising placement. If you suspect a context is causing extra renders, check it with the Profiler (08-05) instead of restructuring blindly.
Exercises
Exercise 1. Classify these six new pieces of CicloUrbano data according to section 2's taxonomy, and state where each should live and with what tool:
- The text the operator types into
WorkshopPage's search box. - The list of incidents for a station, coming from
GET /estaciones/est-02/incidencias. - The chosen price unit (€/hour or €/day), which affects every card and the detail page.
- Whether the
BikeCardcard shows its long description expanded. - The catalogue's sort order (by price or by model), which needs to be shareable by link.
- The number of active bookings shown by the badge in
Header.
Exercise 2. The following component has three state-management problems. Identify them and rewrite it.
function StationsPage() {
const [stations, setStations] = useState([]);
const [district, setDistrict] = useState('todos');
const [visible, setVisible] = useState([]);
const [totalDocks, setTotalDocks] = useState(0);
const [openDetail, setOpenDetail] = useState(null);
useEffect(() => {
setVisible(stations.filter((st) => district === 'todos' || st.district === district));
}, [stations, district]);
useEffect(() => {
setTotalDocks(visible.reduce((sum, st) => sum + st.docks, 0));
}, [visible]);
// …
}Exercise 3. A colleague proposes: "Let's put the whole application in Redux: the bikes, the catalogue filter, the theme, UserMenu's dropdown, and the booking form's draft. That way it's all in one place and we always know where to look." Write a reasoned response, data point by data point, saying which ones should and which shouldn't, and why.
Solutions
Solution 1.
| Data | Type | Where it lives | Tool | Reasoning |
|---|---|---|---|---|
| 1. Workshop search box | Local UI state (or form state) | In WorkshopPage |
useState + useDebounce |
Only that screen uses it, and there's no point sharing it by link. If the team decides it should be linkable after all, it moves to the URL |
| 2. A station's incidents | Server state | Query cache | useQuery(['estaciones', estacionId, 'incidencias']) |
It comes from an API: the tree's first question gets a "yes," and that's where the walk ends |
| 3. Price unit | Application-wide global | Near the root | Context (or catalogueSlice if you're already using Redux) |
Many readers, very infrequent changes: an identical profile to the theme's |
| 4. Expanded description | Local UI state | Inside BikeCard |
useToggle |
A per-card visual detail. Lifting it would force you to keep a map of open identifiers for nothing |
| 5. Catalogue sort order | URL state | In the address | useSearchParams, alongside ?tipo= |
The prompt says so: it needs to be shareable by link |
| 6. Number of active bookings | Derived from server state | Not stored | bookings.filter((b) => b.status === 'activa').length |
It isn't state: it's a count over the booking list. Storing it guarantees that one day it'll show a figure that doesn't match the list |
Case 6 is the most instructive one: the right answer isn't "where do I store it" but "don't store it."
Solution 2. The three problems:
visibleis a derived value stored as state, synced with an effect. Both the state and the effect are unnecessary.totalDocksis a derived value of a derived value, with a second, chained effect. Every change todistricttriggers three renders: one with stale data, one after the first effect, and one after the second.stationsis server state stored inuseState, with no loading, no error, and no cancellation. It's 07-06's candidate.
function StationsPage() {
// 3) Server state: in 07-06 this becomes useQuery
const { data: stations = [], isPending, isError } = useStations();
// Real state: only two pieces
const [district, setDistrict] = useState('todos');
const [openDetail, setOpenDetail] = useState(null);
// 1) and 2) Derived values: expressions, not state
const visible = stations.filter((st) => district === 'todos' || st.district === district);
const totalDocks = visible.reduce((sum, st) => sum + st.docks, 0);
if (isPending) return <LoadingIndicator message="Loading stations…" />;
if (isError) return <Notice tone="error" text="Couldn't load the stations." />;
// …
}Five state variables become two. Both useEffects disappear, and with them the intermediate renders and the windows of inconsistency. openDetail stays: it's legitimate local UI state.
Solution 3. A reasoned response, data point by data point:
- Bikes: no. They're server state. In Redux you'd have to hand-write loading, error handling, cancellation, revalidation on returning to the tab, and invalidation after every write. They belong in a query cache (07-06). If the team would rather not add another dependency, the reasonable alternative is RTK Query, which is the same idea inside Redux, not a hand-written
slice. - Catalogue filter: no. It already lives in the URL, which is the correct source of truth because it needs to be shareable by link and survive a reload. Moving it into the store would create a second copy and a syncing effect, with guaranteed inconsistency.
- Theme: not needed. It changes once per session and gets read by a
ThemeButtonplus the document'sdata-themeattribute. Context solves it with no dependencies. Putting it in Redux isn't wrong, but it adds nothing: there's no logic to audit and no actions to trace. UserMenu's dropdown: absolutely not. It's local UI state. Putting it in the store would turn it global; every opening would dispatch an action that clutters the DevTools history, and it would force you to check who else reads that field before touching it.- The form's draft: no. It changes on every keystroke. In the store, every key would be an action and a comparison cycle for every subscriber. It stays next to the form, in
useStateor in its reducer. - What genuinely belongs in Redux: the session (
sessionSlice), the persistent catalogue criteria that don't belong in the URL — like the search term, if it ends up shared across screens — and the client-side booking logic, which is the part with transitions worth auditing.
And the underlying argument against the proposal: "having it all in one place" isn't an advantage if that place stops telling you anything. A store with two hundred fields, a hundred and fifty of which are local, is just as hard to reason about as having no store at all; on top of that, it buries Redux's most valuable tool — the action history — under the noise of menus opening.
Conclusion
Before installing anything, you've brought order. You know that managing state means answering three questions for each piece of data — who owns it, who needs it, and when it expires — and that the answer sorts into six types: local UI state, shared among a few, application-wide global, server state, URL state, and form state, each with its natural home and its tool. You have a decision tree that starts with the question that saves the most — "does it come from a server?" — and that leaves the dedicated store as the last branch, the one you reach only after ruling out five cheaper alternatives. You know the two symmetric mistakes and their symptoms: over-lifting, which turns every click into a render of the whole application and fills components with pass-through props, and premature globalization, which puts menuOpen in a store and forces you to touch five files to add a checkbox. And you've reinforced 05-01's rule with more weight behind it: a single source of truth, with everything else computed, because visible, isOperator, canSubmit, bookingTotal, and freeDocks aren't state, and storing them only guarantees that one day they'll contradict each other.
On top of that framework you've placed the real options — props and lifting, context, useReducer + context, Redux Toolkit, Zustand, Jotai, and TanStack Query — with their entry cost and their right moment, and you've done the CicloUrbano audit: most of it is already in the right place, the filter will stay in the URL and the dropdowns will stay local, but BookingsContext mixes form state with data that's actually server state, and src/data/domain.js is pretending to be a database.
The first piece to reorder is the one already in your hands. Context isn't just a mechanism for avoiding props: it's a state-management strategy, with a complete per-domain module pattern, a very concrete performance cost that so far has only been mentioned in passing, and a clear limit beyond which another tool makes more sense. The next lesson is Context API.
React Course
Module 1: Getting Started with React
- What Is React?
- Setting Up the Development Environment
- Hello World in React
- JSX: A JavaScript Syntax Extension
- How React Renders: Virtual DOM and Reconciliation
Module 2: React Components
- Understanding Components
- Function vs Class Components
- Props: Passing Data to Components
- State: Managing Component State
- Styling Components: CSS, Modules and Utilities
Module 3: Working with Events
- Handling Events in React
- Conditional Rendering
- Lists and Keys
- Forms and Controlled Components
- Form Validation and Uncontrolled Components
- Accessibility in Interactive Components
Module 4: Advanced Component Concepts
- Lifting State Up
- Composition vs Inheritance
- React Lifecycle Methods
- Hooks: Introduction and Basic Use
- Error Boundaries: Catching Failures in the UI
Module 5: React Hooks
- The useState Hook
- The useEffect Hook
- The useRef Hook and DOM Access
- The useContext Hook
- The useReducer Hook
- Custom Hooks
Module 6: Routing in React
- Introducing React Router
- Setting Up React Router
- Nested Routes
- Programmatic Navigation
- Protected Routes and Access Control
Module 7: State Management
- Introduction to State Management
- The Context API
- Redux: Introduction and Setup
- Redux: Actions and Reducers
- Redux: Connecting to React
- Server State: Fetching, Caching and Syncing
Module 8: Performance Optimization
- Performance Optimization Techniques in React
- Memoization with React.memo
- The useMemo and useCallback Hooks
- Code Splitting and Lazy Loading
- Measuring Performance with React DevTools Profiler
Module 9: Testing React Applications
- Introduction to Testing
- Unit Testing with Jest
- Component Testing with React Testing Library
- Testing Asynchronous Code and Mocking APIs
- End-to-End Testing with Cypress
Module 10: Advanced Topics
- Server-Side Rendering (SSR) with Next.js
- Static Site Generation (SSG) with Next.js
- Suspense and React Server Components
- TypeScript with React
- React Native: Building Mobile Apps
