Modern websites are not just “pages” anymore. They behave like applications: data loads from APIs, users log in, forms update live, and multiple parts of the UI need to stay in sync. State management is how we store and update the “current situation” of the app (the state) so the UI stays predictable, consistent, and secure.
1) What is “State”?
State is any data that can change while a user is using your site, for example:
- The logged-in user profile
- A shopping cart and totals
- A list of products returned from an API
- A form’s current input values and validation errors
- UI status like “loading…”, “error”, “menu open/closed”
- Which page/tab/filter is currently selected
A useful way to think about it:
- Server state: comes from the back-end (database/API) and may change outside the user’s session (e.g., product stock levels).
- Client state: only exists in the browser/app (e.g., “is the sidebar open?”).
In this lesson we focus on keeping client-side state and data flow clean, especially when it relates to back-end data.
2) Local State vs Global State
Local State (Component/Feature State)
Local state is owned by one component (or one small area of the UI) and does not need to be shared widely.
Examples
- A single modal’s open/close state
- Form input fields inside one page
- “Show password” toggle on a login form
When local state is the best choice
- Only one component needs it
- Passing it around would add unnecessary complexity
- It resets naturally when the component unmounts/navigates away
Benefits
- Easier to reason about
- Less risk of unintended side-effects
- Less code and fewer dependencies
Global State (Shared Application State)
Global state is shared across multiple parts of your app and must be consistent everywhere.
Examples
- Auth status (logged in/out, roles/permissions)
- User profile used in multiple pages
- Shopping cart shown in header and checkout page
- Theme/language settings
- Notification/toast messages used across the app
When global state makes sense
- Multiple components need the same data
- Components are far apart in the component tree
- You need a single “source of truth” to avoid mismatch
Risks of overusing global state
- Harder debugging (many parts of the app can read/write)
- Accidental coupling (unrelated features affecting each other)
- Increased complexity and boilerplate
Rule of thumb
Start with local state. Move to global state only when sharing becomes painful.
3) Predictable Data Flow: “Single Source of Truth”
A predictable UI typically follows:
- State lives in one place (single source of truth)
- UI renders from state
- User actions trigger events
- Events update state
- UI updates automatically
This prevents the common problem where two components keep their own copies of “the same” data and become inconsistent.
Example problem
- Header cart badge shows 3 items
- Cart page shows 2 items
This often happens when the cart is duplicated in multiple places instead of being shared correctly.
4) Props and Events (Passing Data and Changes)
Most front-end frameworks use a pattern like:
- Data goes down (parent → child) via props/inputs
- Events go up (child → parent) via events/callbacks/outputs
This keeps flow clear: parents own the state; children display it and request changes.
Typical pattern
- Parent stores
items - Parent passes
itemsto a child list component - Child emits
addItemorremoveItem - Parent handles the event and updates the state
Why this is good
- The child stays reusable and “dumb” (presentational)
- The parent remains responsible for business logic
- It avoids hidden updates and confusing side-effects
Common pitfall: “Prop drilling”
If you pass the same prop through many levels just to reach a deep child, you get:
- Too much wiring
- Components that “know too much” about the app
- Hard refactoring later
When prop drilling becomes a problem, consider Context/Store/Service approaches (global state tools).
5) State Management Tools (Context/Redux, Pinia/Vuex, Angular Services)
Different frameworks solve global/shared state in different ways. The goal is the same: a central place to read state and update it in controlled steps.
A) React: Context and Redux (and similar libraries)
React Context
- Good for simple global state (theme, auth user, language)
- Allows deep components to read data without prop drilling
- Updates can cause re-renders if not structured well
Redux (or Redux Toolkit)
- A more structured approach for complex apps
- Encourages predictable updates via actions and reducers
- Easier debugging with tooling (time-travel debugging in some setups)
Good use cases
- Large apps with many shared state changes
- Teams needing consistent patterns and strong debugging
B) Vue: Pinia / Vuex
Pinia (recommended in modern Vue)
- Simple store definitions
- Clear actions and state
- Great devtools integration
Vuex (older standard)
- Still used in existing projects
- More boilerplate than Pinia
Good use cases
- Medium to large Vue applications
- Shared state across multiple views/components
C) Angular: Services (often with RxJS) and optional libraries
Angular commonly uses:
- Services for shared state and business logic
- Dependency Injection makes services available across components
- RxJS Observables for streams of state and async data
For very complex apps, teams may also use NgRx, but services alone handle many real-world cases.
Good use cases
- Enterprise apps
- Teams that prefer clear separation: components for UI, services for logic/data
6) Keeping UIs Predictable: Practical Patterns
Pattern 1: Separate UI State from Data State
- UI state: loading flags, open/close, selected tab
- Data state: user profile, product list, API results
This helps avoid messy “all-in-one” state objects that are hard to maintain.
Pattern 2: Normalise Complex Data (Where Needed)
If you store lists of items, it can be helpful to store them like:
itemsById: { 1: {...}, 2: {...} }itemIds: [1, 2]
This makes updates easier (update one item without looping through arrays) and reduces bugs in larger apps.
Pattern 3: Controlled Updates (Avoid “Random Writes”)
Even without Redux, aim for controlled state updates:
- One or a small number of functions are allowed to update shared state
- Components call those functions instead of editing shared objects directly
This is especially important for:
- Auth state
- Permissions/roles
- Any state that affects security-related UI (like admin menus)
7) Server Data and Caching (Back-end Connection)
Back-end fundamentals matter because many state problems come from server calls.
Typical server data lifecycle:
- Set
loading = true - Call API
- On success: store response and set
loading = false - On error: store error and set
loading = false
Key concerns
- Stale data: UI shows old values after an update
- Duplicate requests: multiple components call the same endpoint unnecessarily
- Race conditions: a slower response overwrites a newer one
Good practice
Centralise API access (e.g., a dedicated API module/service) and have consistent patterns for:
- Loading flags
- Error handling
- Retrying or fallback states
- Caching where appropriate
Many projects use dedicated “server state” libraries (depending on framework) to help with caching and synchronisation, but the core idea stays the same: keep server data flow consistent and observable.
8) Security-Sensitive State: Auth and Authorisation
Auth-related state must be handled carefully:
- Do not store sensitive tokens in unsafe places without understanding the risk.
- Make sure UI state does not become the “security layer”.
Important principle:
- The back-end is the final authority for permissions.
- The front-end can hide buttons/links for usability, but the API must still enforce access control.
Example
- Front-end hides “Delete User” button unless role is admin.
- Back-end must still check the role on the delete endpoint.
For state management this means:
- Keep auth state consistent (one source of truth)
- Avoid “guessing” permissions in multiple places
- Use a central function/service to interpret roles/permissions
9) Common Mistakes (and How to Avoid Them)
-
Everything becomes global
- Fix: keep UI and feature state local unless sharing is required.
-
Duplicated state
- Fix: store each piece of state in one place and derive other values (like totals) from it.
-
Mutating state directly
- Fix: update immutably (create new objects/arrays) or follow the framework’s recommended patterns.
-
Inconsistent loading/error handling
- Fix: standardise patterns across the app (same flags, same UI treatment).
-
Mixing business logic into UI components
- Fix: move logic to stores/services and keep components focused on rendering.
10) Quick Checklist: Choosing the Right Approach
Use this to decide what to do with a piece of state:
- Does only one component need it?
- Yes → Local state
- Do multiple distant components need it?
- Yes → Global state (Context/Store/Service)
- Is it mainly coming from the server?
- Consider a central data-fetching layer and consistent caching rules
- Is it security-sensitive (auth/roles)?
- Keep it centralised, consistent, and always backed by server enforcement
Summary
State management is about making your UI predictable by controlling where state lives and how it changes. Prefer local state first, use props/events for clear data flow, and introduce global tools (Context/Redux, Pinia/Vuex, Angular services) when shared state becomes difficult. Keep server-driven data consistent, handle loading and errors systematically, and treat auth-related state with extra care because the back-end must always remain the final authority.