Modeling Application State with Diagrams
Entity relationship diagrams, sequence diagrams, and state diagrams are not documentation exercises -- they are thinking tools that reveal the logic your code has to implement before you write a single line.
The previous post argued that modeling separates complexity you must handle from complexity you introduce yourself. This post covers how to actually do it -- three lightweight diagram types that each answer a different question about your application.
None of these require formal tools. All three can be written in plain text.
Entity Relationship Diagrams: What Data Exists
ERDs document the entities in your system and how they relate. Most developers associate them with databases. They apply equally to your frontend data model -- the objects you pass between components, the shapes you store in state.
For a task management app, an ER sketch might look like this:
User { id, name, email }
Project { id, name, ownerId → User }
Task { id, title, status, projectId → Project, assigneeId → User }
Comment { id, text, taskId → Task, authorId → User }Each arrow shows a relationship. A Task belongs to one Project. A Comment belongs to one Task. These relationships directly tell you which values are IDs (and should be stored as such) vs which values are owned data. A Task does not need to store a copy of the Project -- it stores a projectId.
This is the same principle from Redundant State and When to Use Refs. The ER diagram makes it visible before you write a single hook.
Sequence Diagrams: How Actors Talk to Each Other
Sequence diagrams document the flow of messages between different parts of your application -- the UI, a service layer, an API, an external system.
This is especially useful for mapping what happens outside your React components. The async calls, the service boundaries, the round trips. It answers a question most state management problems hide: what is actually causing what?
A plain-text sequence for task search:
UI → TaskService: search({ query, projectId })
TaskService → API: GET /tasks?q={query}&project={id}
API → TaskService: Task[]
TaskService → UI: filtered results
UI: render task listNo special notation. The value is in writing it down -- seeing which actor is responsible for each step, where errors can surface, and whether there are redundant round trips you had not considered.
When you can see the actor flow, you can also see exactly where React state needs to exist and where it does not. The API layer does not need to know about component state. The component does not need to know how the API call was made.
State Diagrams: What Your Application Can Do
State diagrams document the behavior of a single actor -- one screen, one flow, one component -- by listing its possible states and the transitions between them.
This is the most directly useful diagram for React state management because it forces you to answer: what can this thing actually do at any given moment? Not "what state variables exist" but "what states can this be in, and what can happen in each one?"
A state diagram for a task:
backlog → in_progress (user starts work)
in_progress → in_review (user submits for review)
in_review → in_progress (reviewer requests changes)
in_review → done (reviewer approves)
done → (no transitions)The last line is where this becomes useful. A list of steps would let you go from done back to in_progress. A state diagram forces the decision: is that allowed?
If not, you explicitly model the constraint.
ExpandState diagram for task management showing backlog, in-progress, in-review, and done states with forward transitions and selective back arrows -- done state has no outgoing transitions, shown as an explicit constraint rather than an oversight
This is a directed graph. Not an array of steps, not a doubly linked list. Every "can we go back from here?" question becomes a decision you make in the diagram, not something that surprises you in code.
From Diagram to TypeScript
The state diagram maps directly to a discriminated union. This is one of the most important connections in this series.
type TaskState =
| { status: 'backlog' }
| { status: 'in_progress'; startedAt: string }
| { status: 'in_review'; submittedAt: string }
| { status: 'done'; completedAt: string };Once you have this type, invalid states become unrepresentable. A task cannot be done and in_progress at the same time. There is no isDone && isInProgress boolean combination to debug.
The state diagram tells you what states exist. The discriminated union encodes that into the type system. The compiler becomes your enforcer.
Keep It in the Codebase
All three of these sketches are most useful when they live inside your repository -- version-controlled, close to the code, readable by your teammates and AI tools.
A flows.md file works well. No special format required:
## Entities
User { id, name }
Task { id, title, status, assigneeId → User }
## Task creation sequence
UI → TaskService: createTask({ title, assigneeId })
TaskService → API: POST /tasks
API → TaskService: Task
TaskService → UI: new task
## Task state machine
backlog → in_progress (start)
in_progress → in_review (submit)
in_review → in_progress (changes requested)
in_review → done (approved)This file is not documentation for its own sake. It is a thinking artifact that clarifies the incidental complexity before you write code. A codebase with a rough flows.md is easier to debug, easier to extend, and easier to hand off than one where the flows live only in the developer's head.
The state diagram in particular sets up what comes next. Finite States and Type States goes deeper into encoding valid states directly into TypeScript -- so the compiler enforces exactly the constraints your diagram defines.
The Essentials
- Three diagrams, three questions. ERDs: what data exists and how does it relate? Sequence diagrams: how do actors communicate? State diagrams: what can this thing do, and from which states?
- State diagrams are directed graphs, not lists. The value is in making explicit which transitions are allowed and which are not. The terminal state with no outgoing arrows is a decision, not an oversight.
- Plain text in a
flows.mdinside the repo is enough. No formal tool required. The file becomes context for teammates and AI tools -- version-controlled, close to the code, easy to maintain.
Further Reading and Watching
- State Machines and Statecharts -- Stately Docs: A clear introduction to state machines as a modeling tool, from the team behind XState. Directly extends the diagram approach covered here.
- Visualizing State with XState -- Stately.ai: A walkthrough of building state diagrams visually and generating code from them. Note: verify this YouTube link before publishing.
Keep reading