In Chapter 1: Main UI Controller, we built the Stage Manager (the Controller). In Chapter 2: Interactive Prompt Views, we built the Actors (the Views).
However, if you put actors on stage without instructions, they will just stand there. They need a Script.
In software engineering, specifically for UI, this script is often called a State Machine. It ensures the conversation flows in a logical order and prevents the app from doing two contradictory things at once (like asking for a rating while saying goodbye).
Imagine a traffic light. It has a specific cycle: Green $\to$ Yellow $\to$ Red. It should never jump from Green immediately to Red without a warning, and it should certainly never show Green and Red at the same time.
Our survey has a similar lifecycle:
The Survey Lifecycle State Machine manages these transitions so the rest of your code doesn't have to worry about "what happens next."
First, let's look at the possible "modes" our survey can be in. In useSurveyState.tsx, we define these states plainly:
type SurveyState =
| 'closed' // Survey is invisible
| 'open' // Showing the 1-3 rating stars
| 'thanks' // Showing "Thank You"
| 'transcript_prompt' // Asking for permission
| 'submitting' // Sending data to server
| 'submitted'; // Done sending
This list acts as the single source of truth. At any millisecond, the app is in exactly one of these states.
useSurveyState
We package this logic into a React Hook called useSurveyState. This hook is the brain that holds the current state and provides functions to change it.
You initialize the hook with configuration options, like how long to show the "Thank You" message.
// Inside a parent component
const surveyLogic = useSurveyState({
hideThanksAfterMs: 3000, // Close after 3 seconds
onSelect: (id, rating) => console.log('User picked:', rating),
shouldShowTranscriptPrompt: (rating) => rating === 'bad',
});
The hook returns the variables and functions you need to wire up your UI:
return {
state: surveyLogic.state, // e.g., 'open'
open: surveyLogic.open, // Function to start survey
handleSelect: surveyLogic.handleSelect, // Function when user rates
// ... other handlers
};
The most critical job of this state machine is deciding what happens after a user selects a rating.
This logic lives inside handleSelect.
Let's look at the simplified logic inside the hook:
// Inside handleSelect(selectedRating)
// 1. If user dismissed (pressed 0), just close.
if (selected === 'dismissed') {
setState('closed');
}
// 2. Check if we need to ask for a transcript (e.g., if rating is 'bad')
else if (shouldShowTranscriptPrompt?.(selected)) {
setState('transcript_prompt'); // Switch to the waiver form
}
// 3. Otherwise, just say thanks
else {
showThanksThenClose(); // Switch to 'thanks' -> wait -> 'closed'
}
This if/else block is the core "Script" of our play. It directs the actors where to go based on audience input.
When the survey ends, we don't want it to vanish instantly. We want to show a "Thank You" message for a few seconds.
The state machine handles this pacing automatically using setTimeout.
const showThanksThenClose = () => {
// 1. Show the message
setState('thanks');
// 2. Set a timer to close it later
setTimeout(() => {
setState('closed');
setLastResponse(null); // Reset data
}, hideThanksAfterMs);
};
By keeping this timer logic inside the State Machine, the Main UI Controller doesn't need to know about milliseconds or timeouts. It just renders whatever the state tells it to.
Let's visualize the "Bad Rating" scenario to see how the state updates step-by-step.
One detail we haven't touched on is the appearanceId.
Every time the survey opens, we generate a unique ID (a UUID). This acts like a "Ticket Number" for that specific interaction.
// useSurveyState.tsx
const open = useCallback(() => {
// Don't open if already open
if (state !== 'closed') return;
setState('open');
// Generate a new ID for this specific session
appearanceId.current = randomUUID();
}, [state]);
This ID is passed to all your event handlers (onSelect, onTranscriptSelect). This ensures that if a user rates the survey five times, your database knows they are five separate events, not one event changing over and over.
The Survey Lifecycle State Machine is the invisible brain of our operation.
By abstracting this complex logic into a single hook useSurveyState, our visible UI components stay simple and focused on drawing text to the screen.
Now that we have a Controller, Views, and a State Machine, we have a working survey! But... when does it actually open? We don't want to annoy the user by opening it randomly.
In the next chapter, we will look at how to control when the survey appears using Pacing and Configuration.
Next Chapter: General Pacing and Configuration
Generated by Code IQ