Deep vs Shallow History
Master SCXML history states by building a text editor with suspend/resume functionality. This example demonstrates the critical difference between deep history (restores entire nested state hierarchy) and shallow history (restores only immediate child state).
What you'll learn:
- History state fundamentals:
<history type="deep">vs<history type="shallow"> - Nested state hierarchies and how history interacts with them
- Real-world suspend/resume patterns for applications
- When to choose deep vs shallow history
Try It
Click Start, navigate through the editor states, then suspend and resume with either deep or shallow history:
How to explore:
- Click start to enter the editor
- Navigate:
settings→close→ back to document view - In editing mode:
escape(command) →v(visual) →escape(command) →i(insert) - Click suspend to pause the application
- Compare: resume_deep vs resume_shallow
The Core Concept
When a compound state is exited, history states remember which child states were active. The question is: how much do they remember?
┌─────────────────────────── editor ───────────────────────────┐
│ │
│ ┌─────────────── document ───────────────┐ │
│ │ │ │
│ │ ┌───────── editing ─────────┐ │ │
│ │ │ │ │ │
│ │ │ [insert] [command] [visual] │ [preview] │
│ │ │ ▲ │ │ │
│ │ │ │ Currently active │ │ │
│ │ └─────┼─────────────────────┘ │ │
│ │ │ │ │
│ └────────┼───────────────────────────────┘ │
│ │ │
│ [H*]───┘ Deep history: remembers [document, editing, insert] │
│ [H]────── Shallow history: remembers only [document] │
│ │
└───────────────────────────────────────────────────────────────┘
Deep History (type="deep")
Captures the entire nested state configuration:
- Active:
[editor, document, editing, insert] - After suspend → resume_deep: Returns to
[editor, document, editing, insert] - Use case: Full application restore (like reopening a suspended app exactly where you left off)
Shallow History (type="shallow")
Captures only the immediate child of the compound state:
- Active:
[editor, document, editing, insert] - After suspend → resume_shallow: Returns to
[editor, document]→ then document'sinitial→[editor, document, editing, insert] - Wait—same result? Not always! See the key difference below.
The Key Difference: Settings Detour
The difference becomes clear when you take a detour through settings:
Scenario: Editing in Visual Mode, then Settings
- Start in:
[editor, document, editing, visual] - Send
settingsevent → Go to:[editor, settings] - Send
suspendevent → Exit editor - Now try both resumes:
| History Type | What's Restored | Result |
|---|---|---|
| Deep | Full previous config | [editor, document, editing, visual] |
| Shallow | Only settings (last direct child) |
[editor, settings] |
Deep history remembered you were in visual mode inside editing inside document. Shallow history only remembered you were in settings.
The SCXML Implementation
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
datamodel="ecmascript" initial="idle" name="DeepHistoryDemo">
<datamodel>
<data id="suspendCount" expr="0"/>
</datamodel>
<state id="idle">
<transition event="start" target="editor"/>
</state>
<state id="editor" initial="document">
<!-- Both history types defined -->
<history id="editor_deep_history" type="deep">
<transition target="document"/> <!-- Default if no history -->
</history>
<history id="editor_shallow_history" type="shallow">
<transition target="document"/>
</history>
<transition event="suspend" target="suspended">
<assign location="suspendCount" expr="suspendCount + 1"/>
</transition>
<transition event="settings" target="settings"/>
<!-- Nested document state with further nesting -->
<state id="document" initial="editing">
<state id="editing" initial="insert">
<state id="insert">
<transition event="escape" target="command"/>
</state>
<state id="command">
<transition event="i" target="insert"/>
<transition event="v" target="visual"/>
</state>
<state id="visual">
<transition event="escape" target="command"/>
</state>
</state>
<transition event="preview" target="preview"/>
</state>
<state id="preview">
<transition event="edit" target="document"/>
</state>
<state id="settings">
<transition event="close" target="document"/>
</state>
</state>
<state id="suspended">
<!-- Resume with deep history - full state restored -->
<transition event="resume_deep" target="editor_deep_history"/>
<!-- Resume with shallow history - only direct child restored -->
<transition event="resume_shallow" target="editor_shallow_history"/>
<transition event="quit" target="idle"/>
</state>
</scxml>
Key Implementation Details
Defining History States:
<history id="editor_deep_history" type="deep">
<transition target="document"/> <!-- Default target if no history exists yet -->
</history>
The nested <transition> provides a default target for the first entry (before any history has been recorded).
Transitioning to History:
<transition event="resume_deep" target="editor_deep_history"/>
Targeting a history pseudo-state causes the interpreter to restore the remembered configuration instead of entering a normal state.
Real-World Applications
Application Suspend/Resume
<state id="app" initial="main_menu">
<history id="app_history" type="deep"/>
<transition event="os.suspend" target="suspended">
<script>saveAppState();</script>
</transition>
<!-- Deep nested UI states -->
<state id="main_menu">...</state>
<state id="game">
<state id="playing">
<state id="level1"/>
<state id="level2"/>
</state>
<state id="paused"/>
</state>
</state>
<state id="suspended">
<transition event="os.resume" target="app_history"/>
</state>
Wizard with Back Button
<state id="wizard" initial="step1">
<history id="wizard_history" type="shallow"/>
<state id="step1">
<state id="step1_basic"/>
<state id="step1_advanced"/>
<transition event="next" target="step2"/>
</state>
<state id="step2">
<transition event="back" target="wizard_history"/> <!-- Returns to step1 -->
<transition event="next" target="step3"/>
</state>
</state>
Here, shallow history makes sense—going "back" should return to step1, not necessarily to step1_advanced.
Decision Guide: Deep vs Shallow
| Scenario | Recommended | Reason |
|---|---|---|
| App suspend/resume | Deep | Users expect exact state restoration |
| Wizard back button | Shallow | Return to step, not sub-state of step |
| Tab switching | Shallow | Remember which tab, not scroll position |
| Undo/redo system | Deep | Precise state restoration |
| Error recovery | Deep | Resume exactly where operation failed |
| Menu navigation | Shallow | Remember section, start fresh in it |
Code Generation
Java
scxml-gen deep-history.scxml -t java -o DeepHistoryDemo.java --package com.example
DeepHistoryDemo sm = new DeepHistoryDemo();
sm.start();
// Navigate deep into the hierarchy
sm.send("start"); // → editor.document.editing.insert
sm.send("escape"); // → editor.document.editing.command
sm.send("v"); // → editor.document.editing.visual
// Suspend
sm.send("suspend"); // → suspended
// Deep resume restores visual mode
sm.send("resume_deep"); // → editor.document.editing.visual ✓
// Or shallow resume would go to document's initial
sm.send("resume_shallow"); // → editor.document.editing.insert
JavaScript
import { DeepHistoryDemo } from './DeepHistoryDemo.js';
const sm = new DeepHistoryDemo();
sm.onStateChange(() => {
console.log('Active:', [...sm.getActiveStateIds()]);
});
sm.start();
sm.send('start');
sm.send('escape'); // insert → command
sm.send('v'); // command → visual
sm.send('suspend');
sm.send('resume_deep'); // Back to visual!
C
scxml-gen deep-history.scxml -t c -o deep_history.c
DeepHistoryDemo sm;
DeepHistoryDemo_init(&sm);
DeepHistoryDemo_start(&sm);
DeepHistoryDemo_send(&sm, EVT_START, NULL);
DeepHistoryDemo_send(&sm, EVT_ESCAPE, NULL);
DeepHistoryDemo_send(&sm, EVT_V, NULL);
DeepHistoryDemo_send(&sm, EVT_SUSPEND, NULL);
DeepHistoryDemo_send(&sm, EVT_RESUME_DEEP, NULL); // Full restore
Common Mistakes
1. Forgetting the Default Transition
<!-- ❌ Wrong: No default if history is empty -->
<history id="my_history" type="deep"/>
<!-- ✅ Correct: Provide default target -->
<history id="my_history" type="deep">
<transition target="default_child"/>
</history>
2. Using Deep When Shallow is Appropriate
<!-- ❌ Probably wrong for a wizard -->
<state id="wizard">
<history id="h" type="deep"/> <!-- Too specific for back button -->
...
</state>
<!-- ✅ Better: User expects to restart the step -->
<state id="wizard">
<history id="h" type="shallow"/>
...
</state>
3. Multiple History States with Same Type
You can have both types in the same parent—use them for different purposes:
<state id="app">
<!-- For full restore (os suspend/resume) -->
<history id="app_full_history" type="deep"/>
<!-- For partial restore (switching modes) -->
<history id="app_section_history" type="shallow"/>
</state>
Summary
| Aspect | Deep History | Shallow History |
|---|---|---|
| Remembers | Entire nested hierarchy | Only immediate child |
| Use case | Full application restore | Section navigation |
| Syntax | type="deep" |
type="shallow" |
| Default | shallow (if type omitted) | — |
Files
| File | Description |
|---|---|
| deep-history.scxml | SCXML source file |
| deep-history-player.html | Interactive demo |
Next Steps
- Error Handling - Retry patterns and error events
- Done Events - Compound state completion with donedata
- Invoke Feature - Child machine management
- Traffic Light Tutorial - Delayed events and compound states