When developing the dictation practice feature, playback control state management got complicated. This doc records our discussion on whether to use a state machine (FSM) to solve these problems.
- Playback control has many interdependent boolean states
- Possible logically contradictory state combinations
- Actual bug: accuracy unexpectedly reset to 0%
- Too many states to consider when adding features
Simply put:
- System can only be in one state at any time
- Only specific events can trigger state transitions
- Side effects can be executed during transitions
stateDiagram-v2
[*] --> IDLE
IDLE --> STARTING : Click play
STARTING --> PLAYING : API success
STARTING --> IDLE : API failure
PLAYING --> PAUSED : Click pause
PLAYING --> COMPLETED : Reach sentence end (non-loop)
PLAYING --> LOOP_WAITING : Reach sentence end (loop mode)
PAUSED --> STARTING : Click resume
COMPLETED --> STARTING : Click replay
LOOP_WAITING --> STARTING : Countdown done or skip
STARTING --> IDLE : Switch sentence
PLAYING --> IDLE : Switch sentence
PAUSED --> IDLE : Switch sentence
const [isPlaying, setIsPlaying] = useState(false);
const [isStarting, setIsStarting] = useState(false);
const [isLoopWaiting, setIsLoopWaiting] = useState(true);
// What does this combination mean? Unclearconst playCurrentSegment = useCallback(() => {
setIsStarting(true);
if (isPlaying) {
player.pauseVideo(); // async
setIsPlaying(false);
}
player.seekTo(startTime); // async
setTimeout(() => {
player.playVideo(); // async
setIsPlaying(true); // might be affected by other operations
setIsStarting(false);
}, 100);
}, []);
// What if user clicks rapidly multiple times?
// What if component unmounts within 100ms?Normal flow:
1. Submit answer -> setPracticeState({accuracy: 13})
2. Display 13%
Bug flow:
1. Submit answer -> setPracticeState({accuracy: 13})
2. Some useEffect triggers -> restoreDifficultyState()
3. Restore memory -> setPracticeState({accuracy: 0})
4. Display 0% (wrong)
enum PlaybackState {
IDLE, STARTING, PLAYING, PAUSED, LOOP_WAITING
}
// Can only be one of these at any time, no contradiction
const [state, setState] = useState(PlaybackState.IDLE);const transitions = {
[PlaybackState.IDLE]: ['PLAY'],
[PlaybackState.STARTING]: ['PLAY_SUCCESS', 'PLAY_ERROR'],
[PlaybackState.PLAYING]: ['PAUSE', 'COMPLETE', 'LOOP_END'],
// ...
};it('transitions from IDLE to STARTING', () => {
expect(transition(PlaybackState.IDLE, 'PLAY')).toBe(PlaybackState.STARTING);
});
it('cannot go directly from STARTING to PAUSED', () => {
expect(transition(PlaybackState.STARTING, 'PAUSE')).toBe(PlaybackState.STARTING);
});- Learning curve
- Initial development time
- Might be over-engineering for simple features
Playback control is complex enough to warrant state machine thinking. But we chose manual implementation over XState:
// Multiple booleans combined to represent state
const [isPlaying, setIsPlaying] = useState(false);
const [isStarting, setIsStarting] = useState(false);
const [pausedTime, setPausedTime] = useState<number | null>(null);
const [isLooping, setIsLooping] = useState(false);
const [isLoopWaiting, setIsLoopWaiting] = useState(false);
// State combinations map to FSM states:
// IDLE: !isPlaying && !isStarting && !isLoopWaiting
// STARTING: isStarting
// PLAYING: isPlaying && !isLoopWaiting
// PAUSED: pausedTime !== null
// LOOP_WAITING: isLoopWaitingWhy not XState?
- Team more familiar with React Hooks
- Don't want to add new dependency
- Manual implementation already ensures state consistency (using guard conditions)
Difficulty switching has just 3 states, no complex constraints, useState is enough:
const [difficulty, setDifficulty] = useState(BlanksDifficulty.INTERMEDIATE);When to use state machine? Consider if 3+ apply:
- State count >= 5
- Strict transition constraints
- Possible illegal state combinations
- Complex async operations
- Complex dependencies between states
When not to use?
- State count <= 3
- Few constraints
- Mainly UI display states
- Logic is straightforward
Conclusion: Use FSM concepts rather than FSM libraries. Design with state machine thinking where needed, but don't necessarily add extra dependencies.