WEB ATELIER (UDIT) Β· Learning by doing, with theory, practice and shared reflection

State & UI: The Memory of Interaction

Draft

URL: https://ruvebal.github.io/web-atelier-udit/lessons/en/react/state-and-ui/

πŸ“‹ Table of Contents

β€œ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.

  1. Implement isLoading, hasError, hasData as separate booleans
  2. Create a scenario where all three are true simultaneously
  3. Refactor to use a single status field 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.

  1. Draw the statechart: loggedOut β†’ loggingIn β†’ loggedIn (with errors, retry)
  2. Define all transitions: what event triggers each?
  3. Define guards: what conditions must be true?
  4. 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

Further Exploration


πŸ”— Lesson Navigation

Previous Current Next
AI-Assisted Development Foundations State & UI React Programming Fundamentals

β€œUn solo estado evita mil conjeturas β€” paz en el render.”