State & UI: The Memory of Interaction
DraftURL: https://ruvebal.github.io/web-atelier-udit/lessons/en/react/state-and-ui/
π Table of Contents
- Learning Objectives
- What Is State? (Two Perspectives)
- The State Taxonomy
- State Machines: From Theory to Practice
- Canonical FSM Diagram:
idle β loading β success/error - The Evolution of State Management
- Why React Changed Everything
- Common State Antipatterns
- Modern State Concepts
- Practical Examples
- Didactic Sequence
- Hands-On Activities
- Koans and Haikus
- References
- π Lesson Navigation
βState is the memory of a machine; in a UI, it is the memory of the userβs intent made visible.β
Learning Objectives
By the end of this lesson, you will be able to:
| Objective | Bloomβs Level |
|---|---|
| Define βstateβ in both theoretical (FSM) and practical (UI) contexts | Understand |
| Classify different types of state (UI, form, server, URL, shared) | Analyze |
| Model a UI flow as a finite state machine | Apply |
| Identify and refactor state antipatterns | Evaluate |
| Choose appropriate state management tools for different scenarios | Create |
What Is State? (Two Perspectives)
The Machine Perspective (Formal Model)
A finite set of internal conditions that determine behavior in response to events.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FINITE STATE MACHINE (FSM) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β STATES Discrete conditions (idle, loading, etc.) β
β TRANSITIONS Edges between states β
β EVENTS Triggers for transitions β
β GUARDS Conditions that allow/block transitions β
β EFFECTS Side effects on transition (fetch, log) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The UI Perspective (Practical Reality)
The current snapshot of data and UI that determines what is displayed and how the interface responds to user actions.
// UI State is multidimensional:
const appState = {
// π DATA: What we're displaying
tasks: [{ id: 1, text: 'Learn state', done: false }],
searchResults: [],
// π¦ FLAGS: What's happening right now
isLoading: false,
isSubmitting: false,
hasError: false,
// π€ IDENTITY: Who's here
user: { id: 'abc', role: 'admin' },
// π META: Information about information
errors: [],
validation: { email: 'valid', name: 'too short' },
pagination: { page: 1, total: 10 },
// π ENVIRONMENT: External conditions
isOnline: true,
theme: 'dark',
viewport: 'desktop',
};
π Didactic Epigraph > βState is the memory of interaction: what has already happened + what the user expects to happen.β
The State Taxonomy
Most state bugs come not from βbad stateβ but from mixing incompatible state types. A useful taxonomy:
| Type | What It Contains | Where It Lives | Examples |
|---|---|---|---|
| UI State | Local component state | useState / component |
Active tab, modal open, focus |
| Form State | Input values, validation | Form library or local | Field values, errors, dirty/touched |
| Server State | Remote data | Cache layer | Lists, entities, permissions |
| URL State | Navigation + filters | Browser URL | Page, search query, filters |
| Shared State | Cross-component data | Context / Store | Auth, theme, cart |
π― The Golden Rule
If a piece of data must survive page reload or be shareable via link, it probably belongs in URL state or server state, not in local state.
// β Wrong: filter in local state
const [filter, setFilter] = useState('active');
// β
Right: filter in URL
const [searchParams, setSearchParams] = useSearchParams();
const filter = searchParams.get('filter') || 'all';
State Machines: From Theory to Practice
Mapping FSM Concepts to UI
| FSM Concept | UI Equivalent | Example |
|---|---|---|
| State | Coherent UI snapshot | idle, loading, success, error |
| Transition | User action or async response | click, HTTP response, timer |
| Guard | Validation before transition | if (form.isValid) |
| Effect | Side effect on transition | fetch, navigate, track analytics |
Why State Machines Matter for UI
Modeling a complex flow as an FSM eliminates impossible states:
// β The Boolean Explosion Problem
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [hasData, setHasData] = useState(false);
// Possible combinations: 2Β³ = 8 states
// Valid combinations: 4 (idle, loading, success, error)
// Invalid combinations: 4 (e.g., loading + error + data = ???)
// β
The FSM Solution
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState < Status > 'idle';
// Possible states: 4
// Invalid states: 0
FSM vs Statecharts
| FSM (Simple) | Statecharts (Harel) |
|---|---|
| 1 active state | Nested states (hierarchy) |
| Linear flows | Parallel states (orthogonal) |
| No memory | History (return to previous) |
| Good for simple flows | Good for real UI complexity |
Why Statecharts for UI?
Real UIs are concurrent systems: βmodal openβ + βfetch in progressβ + βuser typingβ can all be true simultaneously. With boolean flags, you multiply combinations. With statecharts, you control the state space.
Canonical FSM Diagram: idle β loading β success/error
stateDiagram-v2
[*] --> idle
idle --> loading: FETCH
loading --> success: RESOLVE
loading --> error: REJECT
error --> loading: RETRY
success --> loading: REFRESH
π Didactic Note This diagram is not decorative. It defines allowed logic. Any transition not shown is forbidden. The diagram is the specification.
The Evolution of State Management
| Era | Approach | State Location | Pros | Cons |
|---|---|---|---|---|
| 1. Server Render | Multi-page apps | Server (session) | Simple mental model | Full page reloads |
| 2. jQuery DOM | Implicit state | DOM + globals | Quick prototypes | βSpaghetti stateβ |
| 3. SPA + MVC/MVVM | Client-side architecture | Structured models | Better organization | Complex patterns |
| 4. Flux/Redux | Unidirectional flow | Centralized store | Predictable, debuggable | Boilerplate |
| 5. React Hooks | Composable primitives | Component + context | Ergonomic, flexible | useEffect pitfalls |
| 6. Statecharts (XState) | Formal modeling | Machine definitions | Robust, visualizable | Learning curve |
| 7. Server State (React Query) | Cache + sync | Dedicated cache | Solves async complexity | Another layer |
| 8. Signals | Fine-grained reactivity | Reactive atoms | Minimal re-renders | Paradigm shift |
The Arc of History
Server owns state β Client owns state β Client + Server share state with sync layer
β β β
Simple Complex Specialized tools
Why React Changed Everything
React introduced a paradigm shift in how we think about UI:
The Declarative Model
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β THE REACT EQUATION β
β β
β UI = f(state) β
β β
β "The interface is a pure function of the state" β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Before React
// Imperative: describe HOW to update
button.addEventListener('click', () => {
const span = document.getElementById('count');
span.textContent = parseInt(span.textContent) + 1;
});
After React
// Declarative: describe WHAT the UI should be
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
}
// React figures out the HOW (Virtual DOM, reconciliation)
Key Innovations
| Concept | What It Means | Why It Matters |
|---|---|---|
| Virtual DOM | In-memory diff before real DOM update | Efficient updates |
| Declarative Rendering | Describe output, not mutations | Predictable UI |
| Hooks (2019) | Composable state logic | Reusable patterns |
| Concurrency | Prioritized rendering | Responsive UX |
π₯ Guiding Insight > βDonβt synchronize the DOM; synchronize the state. When state is correct, UI follows naturally.β
Common State Antipatterns
β Antipattern 1: Derived State Stored as State
Storing a value that can be computed from other state creates synchronization bugs.
// β Wrong: storing derived value
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0); // Derived!
function addItem(item) {
setItems([...items, item]);
setTotal(total + item.price); // Can get out of sync!
}
// β
Right: compute on render
const [items, setItems] = useState([]);
const total = items.reduce((sum, item) => sum + item.price, 0);
β Antipattern 2: Boolean Flag Explosion
Multiple boolean flags create impossible states.
// β Wrong: flags that can conflict
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [hasData, setHasData] = useState(false);
// What if all three are true? π€―
// β
Right: discriminated union
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success', data: Data }
| { status: 'error', error: Error };
β Antipattern 3: Uncontrolled Effects
fetch calls scattered everywhere create race conditions.
// β Wrong: fetch inside component body
useEffect(() => {
fetchData().then(setData); // What if component unmounts?
}, [query]); // What if query changes before response?
// β
Right: controlled with abort/cancellation
useEffect(() => {
const controller = new AbortController();
fetchData({ signal: controller.signal }).then(setData);
return () => controller.abort();
}, [query]);
// β
Better: use React Query/SWR
const { data, isLoading, error } = useQuery(['data', query], fetchData);
β Antipattern 4: Stale Closures
Event handlers capturing outdated state.
// β Wrong: closure captures stale count
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(count + 1); // 'count' is stale after 1 second
}, 1000);
};
// β
Right: functional update
const handleClick = () => {
setTimeout(() => {
setCount((c) => c + 1); // Uses current value
}, 1000);
};
Modern State Concepts
1. Immutability + Reducers
Principle: Never mutate state directly. Return new state objects.
Benefits:
- Predictable updates
- Time-travel debugging
- Safe concurrent rendering
// Reducer pattern (FSM-like)
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'RESET':
return { ...state, count: 0 };
default:
return state;
}
}
2. Unidirectional Data Flow
βββββββββββββββββββββββββββββββββββββββββββ
β β
β ββββββββββββββββββββββββββββββββ β
β β STATE β β
β ββββββββββββββββ¬ββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββ β
β β VIEW β β
β ββββββββββββββββ¬ββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββ β
β β ACTIONS β β
β ββββββββββββββββ¬ββββββββββββββββ β
β β β
βββββββββββββββββββββ β
β
Data flows in one direction only ββββββββββ
3. Separation of Concerns
| Concern | Where It Belongs |
|---|---|
| Pure state logic | Reducers, state machines |
| Side effects | useEffect, services, machine invocations |
| Derived data | Selectors, computed values |
| UI rendering | Components |
4. Server State as a Distinct Category
React Query / SWR recognize that server data has unique concerns:
// Server state concerns
const {
data, // Cached value
isLoading, // First fetch
isFetching, // Any fetch (including refetch)
isStale, // Needs revalidation?
error, // What went wrong?
refetch, // Manual refresh
} = useQuery(['todos'], fetchTodos, {
staleTime: 5000, // 5s before stale
cacheTime: 300000, // 5min in cache after unmount
});
Practical Examples
Example 1: Counter in Vanilla JS
<button id="btn">
Contador:
<span id="count">0</span>
</button>
<script>
let count = 0; // State: a variable
const btn = document.getElementById('btn');
const span = document.getElementById('count');
btn.addEventListener('click', () => {
count++; // Update state
span.textContent = count; // Manually sync UI
});
</script>
Key Observation: State is implicit. Synchronization is manual.
Example 2: Counter in React (useState)
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>Contador: {count}</button>;
}
Key Observation: State is explicit. UI automatically reflects state.
Example 3: Counter with Reducer (FSM-like)
import { useReducer } from 'react';
type State = { count: number };
type Action = { type: 'INCREMENT' } | { type: 'RESET' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
);
}
Key Observation: State transitions are explicit actions. Reducer is pure and testable.
Example 4: Fetch State as FSM
// Discriminated union pattern in plain JS: use status + data/error
function reducer(state, action) {
switch (action.type) {
case 'FETCH':
return { status: 'loading' };
case 'RESOLVE':
return { status: 'success', data: action.data };
case 'REJECT':
return { status: 'error', error: action.error };
default:
return state;
}
}
Key Observation: Status is a discriminated union. No impossible states.
Example 5: XState (Formal State Machine)
import { createMachine } from 'xstate';
export const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
states: {
idle: { on: { FETCH: 'loading' } },
loading: {
on: {
RESOLVE: 'success',
REJECT: 'error',
},
},
success: { on: { FETCH: 'loading' } },
error: { on: { RETRY: 'loading' } },
},
});
Key Observation: The state machine is the source of truth. It can be visualized and tested.
Didactic Sequence
A recommended learning progression:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LEARNING PROGRESSION β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. COUNTER (useState) β
β β Understand local state, updates, and re-renders β
β β
β 2. FORM (validation as guards) β
β β Understand derived state, errors as meta-state β
β β
β 3. ASYNC FETCH (idle/loading/success/error) β
β β Model as FSM, understand discriminated unions β
β β
β 4. REDUCER PATTERN (useReducer) β
β β Pure state transitions, actions, testability β
β β
β 5. FORMAL STATE MACHINES (XState) β
β β Parallel states, hierarchy, visualization β
β β
β 6. SERVER STATE (React Query / SWR) β
β β Cache, revalidation, stale-while-revalidate β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Hands-On Activities
π¬ Activity 1: Boolean Flags vs FSM
Task: Create a fetch component with boolean flags, then break it.
- Implement
isLoading,hasError,hasDataas separate booleans - Create a scenario where all three are
truesimultaneously - Refactor to use a single
statusfield with FSM pattern
Reflection: What changed? Which is easier to reason about?
π¬ Activity 2: Testable Reducer
Task: Write pure reducer tests.
describe('counterReducer', () => {
it('increments correctly', () => {
const state = { count: 0 };
const action = { type: 'INCREMENT' };
const next = reducer(state, action);
expect(next.count).toBe(1);
});
it('ignores invalid actions', () => {
const state = { count: 5 };
const action = { type: 'UNKNOWN' };
const next = reducer(state, action);
expect(next).toBe(state); // Same reference = no change
});
});
π¬ Activity 3: Diagram First, Code Second
Task: Design a login flow.
- Draw the statechart:
loggedOut β loggingIn β loggedIn(with errors, retry) - Define all transitions: what event triggers each?
- Define guards: what conditions must be true?
- Only then: implement in code
Deliverable: Mermaid diagram + XState or reducer implementation.
π¬ Activity 4: Server State Analysis
Task: Build a paginated list.
Questions to answer:
- What state lives in the URL?
- What lives in cache?
- What is local UI state?
Draw boundaries clearly before coding.
Koans and Haikus
π§ Koan 1
βWrite code for humans first, computers second; the Tao lies in balancing both.β
π Haiku 1: The Flow of State
Estado fluye ya memoria de intenciΓ³n la UI respira
Translation: State already flows / memory of intention / the UI breathes
π§ Koan 2
βExperience is simply the name we give to our bugs after we fix them.β
π Haiku 2: The Diagram
Diagrama en mano la mΓ‘quina canta su ruta bugs en silencio
Translation: Diagram in hand / the machine sings its route / bugs in silence
π§ Koan 3
βQuien guarda diez banderas para un solo flujo, termina programando la excepciΓ³n como producto.β
Translation: βWho stores ten flags for a single flow, ends up programming exceptions as the product.β
π Haiku 3: The One State
Un solo estado evita mil conjeturas paz en el render
Translation: A single state / avoids a thousand guesses / peace in the render
References
Essential Documentation
Conceptual Reading
- Elm Architecture Guide β The origin of unidirectional data flow
- XState Visualizer β See your state machines
- MDN JavaScript Guide
Further Exploration
- Signals: Solid.js, Preact Signals
- Statecharts Theory: David Harelβs original paper
π Lesson Navigation
| Previous | Current | Next |
|---|---|---|
| AI-Assisted Development Foundations | State & UI | React Programming Fundamentals |
βUn solo estado evita mil conjeturas β paz en el render.β