Implementation worksheet · 5 min read
A React Loading State Contract for In-App Guidance
Model every guided surface as four states rather than a loading boolean: loading (nothing to point at), empty (rendered, but no data — guidance may differ or should not run), ready (the real thing, guidance may run), and error (guidance must not run). Then write the contract: guidance only evaluates in ready, and a surface must expose which state it is in rather than leaving guidance to infer it from the DOM. A boolean isLoading collapses empty and ready into one state, which is why tours so often point at an empty table and explain a feature the user cannot see.
React components usually track loading as a boolean and treat everything else as loaded. That works for rendering and fails for guidance, because guidance cares about a distinction rendering does not make: whether there is anything meaningful on screen. A skeleton and an empty state and a populated table are all not-loading, and a tour that fires on not-loading will eventually fire on all three.
Put it into practice
1. Replace the boolean with a named state
A discriminated union — loading, empty, ready, error — rather than isLoading plus optional data. This is a small refactor with an outsized effect: every consumer, including guidance, now has to handle the states explicitly, and the empty case stops being an accident of falsy data.
2. Expose the state to the guidance layer deliberately
Guidance should read state from the component, not infer it by looking for a spinner in the DOM. Inference works until someone changes the spinner. A context value, a data attribute, or a registration call — the mechanism matters less than the fact that it is declared rather than sniffed.
3. Gate guidance on ready, with an explicit empty policy
Most guidance should not run in empty. Some should — 'here is how to add your first one' is exactly the right message for an empty table. Decide per piece of guidance, write it down, and make empty an allowed value rather than an oversight.
4. Never run guidance in error
A tour that starts cheerfully over a failed request is the worst version of this. Error state must block guidance unconditionally, and that rule is worth enforcing centrally rather than per component, because it will be forgotten once per new feature otherwise.
5. Handle the state changing mid-guidance
A user can start a tour in ready and hit an error on the next action. Guidance needs to end when its surface leaves ready, not continue against a screen that no longer matches the copy. Subscribe to the state rather than reading it once at start.
6. Test the slow path on purpose
Force each state and assert guidance behaviour in all four. Local development almost always renders fast enough to hide the loading case entirely, which is why this bug reaches production so reliably.
The four states
Copy this structure into your review document and record your observed result for each row.
| State | What is on screen | Guidance allowed | Tested |
|---|---|---|---|
| loading | skeleton or spinner | no | |
| empty | real UI, no data | by explicit decision | |
| ready | real UI with data | yes | |
| error | error surface | never | |
| ready → error mid-tour | error surface | end the tour | |
| ready → empty mid-tour | empty surface | re-evaluate |
A failure worth checking
Guidance anchored to a skeleton. The skeleton has the same layout and often the same class names as the real component, so the selector matches and the tooltip renders perfectly — over a grey placeholder box. It looks fine in review on a fast connection and is reported by users on slow ones as 'the tour points at nothing'. The four-state contract makes this case representable and therefore testable, which a loading boolean does not.
Common questions
Is this not over-engineering a loading flag?
It would be, if rendering were the only consumer. The moment something else — guidance, analytics, an empty-state CTA — needs to distinguish empty from ready, the boolean starts being worked around at each call site. The union moves that decision to one place.
What about components that stream in data progressively?
Then ready needs a threshold: enough data present for the guidance to make sense. Define it per surface. Partial-render cases are exactly where an inferred state gets it wrong, which is the argument for declaring it.
Basis and scope
This is a proposed implementation method using illustrative examples, not a measured benchmark or a customer case study. Prepared with AI assistance. Validate product-specific behavior against current documentation and your own test environment.