State Management
As a user moves through a questionnaire, their answers need to be stored, validated, and used to determine the next step. Birdhouse handles this through two layers: the qnr-framework state manager (in-memory, per-questionnaire) and the app’s Redux store (persistent, per-vertical).
How Answers Are Tracked
Section titled “How Answers Are Tracked”The State Manager (qnr-framework)
Section titled “The State Manager (qnr-framework)”The useStateManager hook from qnr-framework provides the core answer-tracking logic. It manages a simple key-value store where:
- Key = questionId (e.g.,
dateOfBirth,plan,coverageStartDate) - Value = the user’s answer (string, number, object, array — depends on question type)
When the user answers a question, the flow is:
flowchart LR
A["User submits<br/>answer"] --> B["updateState()<br/>stores answer"]
B --> C["autoRemove<br/>clears dependent<br/>answers"]
C --> D["Rules evaluated<br/>to find next<br/>question"]
D --> E["Next question<br/>rendered"]
The Redux Store (app layer)
Section titled “The Redux Store (app layer)”The app wraps the qnr-framework state manager with a Redux reducer (genericQuestionnaire). This provides:
- Persistence: Answers survive page refreshes and browser navigation
- Per-vertical isolation: Each vertical’s answers are stored separately
- Cross-component access: Other parts of the app (checkout, processing, analytics) can read the answers
Key Behaviors
Section titled “Key Behaviors”autoRemove — Clearing Dependent Answers
Section titled “autoRemove — Clearing Dependent Answers”When a user changes an earlier answer, downstream answers that depended on it may no longer be valid. The autoRemove behavior handles this automatically.
Example: A user selects “Employed” for employment status, then answers salary questions. If they go back and change to “Student”, the salary answers are automatically removed because they no longer apply.
This is configured through removeAnswersLogic — a per-question configuration that specifies which answers should be cleared when a particular answer changes:
| Config | Meaning |
|---|---|
removeAnswersLogic: { employmentStatus: ['salary', 'employer'] } |
When employmentStatus changes, remove salary and employer answers |
Without this, stale answers would persist and could cause incorrect pricing or confusing pre-filled values if the user navigates back and forth.
autoFilterRequiredAnswers
Section titled “autoFilterRequiredAnswers”This behavior removes answers for questions that are not marked as required. It is useful when the question list changes dynamically — for example, when a rule skips over optional questions. Non-required questions that the user never saw should not have stale answers in the state.
isComplete() — Checking Flow Completion
Section titled “isComplete() — Checking Flow Completion”The isComplete() function checks whether all required questions have been answered. This determines:
- Whether the user can proceed to checkout
- Whether a progress indicator shows “complete”
- Whether the questionnaire state is valid for submission
It works by iterating through all questions marked as required: true and checking if an answer exists in the state for each one.
State Flow Diagram
Section titled “State Flow Diagram”flowchart TD
A["User answers question"] --> B["onAnswer callback fires"]
B --> C["stateManagerHelper.updateState()"]
C --> D["Answer stored in state<br/>{questionId: answer}"]
D --> E["autoRemove runs:<br/>clears dependent answers<br/>based on removeAnswersLogic"]
E --> F["Redux action dispatched:<br/>stores in persistent store"]
F --> G["Rules engine evaluates<br/>to determine next question"]
G --> H["isComplete() checked<br/>to update progress"]
What Gets Stored
Section titled “What Gets Stored”The answer format depends on the question type:
| Question type | Answer format | Example |
|---|---|---|
| Radio / Select | String | "PREMIUM" |
| Text / Email | String | "user@example.com" |
| Date | String (ISO) | "1990-05-15" |
| Number / Currency | Number | 45000 |
| Boolean / Toggle | Boolean | true |
| Address | Object | { street: "...", city: "...", postalCode: "..." } |
| Multi-select | Array | ["GLASS", "BICYCLE"] |
| Quote (plan selection) | Object | { plan: "PREMIUM", addons: [...], options: {...} } |
Practical Implications
Section titled “Practical Implications”Navigating Back
Section titled “Navigating Back”When a user goes back to a previous question, their earlier answer is still in state and pre-fills the input. If they change it, autoRemove cleans up any dependent answers.
Browser Refresh
Section titled “Browser Refresh”Because the Redux store persists (via local storage or session storage), users do not lose their progress on page refresh. The questionnaire resumes from where they left off.
Multiple Verticals
Section titled “Multiple Verticals”Each vertical’s answers are isolated in the Redux store. Starting a household questionnaire does not affect answers from a previous dental questionnaire.
Debugging State Issues
Section titled “Debugging State Issues”If a questionnaire behaves unexpectedly (wrong question shown, incorrect pricing, incomplete progress), the first thing to check is the answer state. In the browser’s Redux DevTools, look at the genericQuestionnaire slice to see exactly what answers are stored and whether any stale values are present.
See also
Section titled “See also”- Question Types Reference – what answer format each question type stores
- Configure Validations – how validations interact with state
- Questionnaire Lifecycle – the full journey from CMS to rendered flow