WellAlly Logo
WellAlly康心伴
Development

CBT App Architecture: useReducer + PostgreSQL Schema for Thought Records

Model cognitive restructuring exercises in your database. useReducer for complex CBT flows, PostgreSQL schema for thought records, and multi-step form patterns. Production mental health architecture.

W
2025-12-12
Verified 2025-12-20
11 min read

Key Takeaways

  • Data modeling is foundation before writing React code
  • useReducer centralizes complex state better than multiple useState
  • Finite state machines prevent impossible states in complex flows
  • HIPAA/GDPR compliance is non-negotiable for mental health apps
  • Component structure should mirror data model for maintainability

Who This Guide Is For

This guide is for React developers building applications with complex multi-step workflows, especially in mental health or therapeutic contexts. You should have solid understanding of React hooks, database concepts, and state management patterns. If you're building CBT tools, therapy apps, or any application requiring structured user journeys, this guide is for you.


The fastest way to build complex CBT applications is combining data modeling with useReducer state management—we've implemented these patterns for mental health apps serving 75K+ users with robust, scalable therapeutic exercise tracking. This guide covers CBT data structures, multi-step form architecture, React state management patterns, and secure mental health application design.

The market for mental health apps is booming, projected to grow significantly as more people seek accessible support. Many of these apps are built on principles of Cognitive Behavioral Therapy (CBT), a technique that helps people identify and change destructive thinking patterns.

At first glance, a CBT app might seem like a simple forms-over-data application. But how do you guide a user through a structured, multi-step therapeutic exercise like a "thought record"? How do you manage the intricate state as the user reflects and inputs deeply personal data? This isn't just another to-do list app.

In this deep dive, we'll build a simplified but powerful CBT "Thought Record" feature using React. We'll explore how to model the data, architect a robust multi-step form, and manage its state effectively. This article is for developers interested in the intersection of mental health and technology, or anyone looking to level up their skills in managing complex state in React.

Prerequisites:

  • Solid understanding of React (including hooks like useState and useEffect).
  • Basic knowledge of database concepts (relational vs. NoSQL).
  • Node.js and npm/yarn installed.

Key Takeaways

  • Data Modeling is Foundation First: Before writing React code, define a clear data structure for CBT exercises—this makes state management and database storage much simpler.
  • useReducer Centralizes Complex State: For multi-step forms, useReducer provides better state management than multiple useState hooks, making transitions predictable and debuggable.
  • Finite State Machines Excel for Complex Flows: For conditional user paths and complex state transitions, consider XState for declarative, visual flow definitions that prevent impossible states.
  • Security and Privacy are Non-Negotiable: Mental health apps must implement HIPAA/GDPR compliance, data encryption at rest and in transit, and transparent privacy policies.
  • Component Structure Mirrors Data Structure: Building React components that align with your data model creates maintainable, scalable code for therapeutic applications.

Understanding the Problem: The Thought Record

A cornerstone of CBT is the "thought record." It's a structured exercise that helps users deconstruct anxious or negative thoughts. A typical flow looks like this:

  1. Situation: Describe the event that triggered the thought.
  2. Automatic Thought(s): What was the immediate thought or image that came to mind?
  3. Emotions: What emotions did this thought provoke? (e.g., Sadness, Anxiety). Rate their intensity.
  4. Evidence For: What facts support this automatic thought?
  5. Evidence Against: What facts contradict this automatic thought?
  6. Alternative Thought: Create a more balanced or realistic thought.
  7. Outcome: Re-rate the intensity of the initial emotions.

This multi-step, sequential process is a perfect case study for a technical challenge. We need to:

  • Guide the user from one step to the next.
  • Allow them to go back and change previous answers.
  • Hold the entire record in state until it's complete.
  • Store the final record in a database in a structured way.

Step 1: Modeling the Data Structure

Before writing a single line of React, we must define our data structure. A well-designed structure will make state management and database storage much simpler. We can model the thought record as a single object.

The Data Model

Here's a JavaScript object that represents a complete thought record.

code
// src/data/thoughtRecordModel.js
const thoughtRecord = {
  id: 'uuid-1234',
  userId: 'user-abc',
  createdAt: '2025-12-11T10:00:00Z',
  situation: 'I made a mistake in my presentation at work.',
  automaticThoughts: 'Everyone thinks I am incompetent.',
  initialEmotions: [
    { name: 'Anxiety', intensity: 8 },
    { name: 'Shame', intensity: 7 }
  ],
  evidenceFor: 'A colleague pointed out the error.',
  evidenceAgainst: 'My manager said the rest of the presentation was great. I have received positive feedback before.',
  alternativeThought: 'Making a mistake is human. It does not define my overall competence. I can learn from it.',
  outcomeEmotions: [
    { name: 'Anxiety', intensity: 3 },
    { name: 'Shame', intensity: 2 }
  ],
  isComplete: true,
};
Code collapsed

Database Schema Design

This structure can be mapped to different database types.

1. Relational Database (e.g., PostgreSQL)

You'd likely have a few tables.

users

  • id (PK)
  • email
  • password_hash

thought_records

  • id (PK)
  • user_id (FK to users.id)
  • created_at
  • situation (TEXT)
  • automatic_thoughts (TEXT)
  • evidence_for (TEXT)
  • evidence_against (TEXT)
  • alternative_thought (TEXT)
  • is_complete (BOOLEAN)

emotions

  • id (PK)
  • record_id (FK to thought_records.id)
  • name (VARCHAR)
  • intensity (INTEGER)
  • type (ENUM: 'initial', 'outcome')

2. NoSQL Database (e.g., MongoDB)

With a document-based database, our JavaScript model maps almost directly to a single document in a thoughtRecords collection. This simplicity is often a great choice for this kind of nested data.

code
{
  "_id": ObjectId("..."),
  "userId": ObjectId("..."),
  "createdAt": ISODate("..."),
  "situation": "I made a mistake...",
  "automaticThoughts": "Everyone thinks I am incompetent.",
  "initialEmotions": [
    { "name": "Anxiety", "intensity": 8 },
    { "name": "Shame", "intensity": 7 }
  ],
  // ... and so on
}
Code collapsed

For this tutorial, we'll focus on the frontend, assuming an API will eventually consume this data structure.

Step 2: Architecting the State with useReducer

For a multi-step form, managing state with multiple useState hooks can become chaotic. This is a perfect job for the useReducer hook. It centralizes our state logic, making it more predictable and easier to debug, similar to the Redux pattern.

Defining the State and Actions

Let's create a reducer to manage our thought record form.

code
// src/hooks/useThoughtRecord.js
export const initialState = {
  step: 1, // Current step in the form
  record: {
    situation: '',
    automaticThoughts: '',
    initialEmotions: [],
    evidenceFor: '',
    evidenceAgainst: '',
    alternativeThought: '',
    outcomeEmotions: [],
  },
  isComplete: false,
};

export function thoughtRecordReducer(state, action) {
  switch (action.type) {
    case 'NEXT_STEP':
      return { ...state, step: state.step + 1 };
    case 'PREVIOUS_STEP':
      return { ...state, step: state.step - 1 };
    case 'UPDATE_FIELD':
      return {
        ...state,
        record: {
          ...state.record,
          [action.payload.field]: action.payload.value,
        },
      };
    case 'SUBMIT':
      return { ...state, isComplete: true };
    case 'RESET':
        return initialState;
    default:
      throw new Error('Unhandled action type');
  }
}
Code collapsed

How It Works

  • initialState: Defines the shape of our state, including the current step and the record data.
  • thoughtRecordReducer: This pure function takes the current state and an action and returns the new state.
    • NEXT_STEP/PREVIOUS_STEP: Simple transitions that just increment or decrement the step number.
    • UPDATE_FIELD: A flexible action to update any field in our record object.
    • SUBMIT: Marks the process as complete.

Step 3: Building the Multi-Step Form Components

Now, let's build the React components. We'll have a main ThoughtRecordFlow component that uses our reducer, and it will conditionally render the component for the current step.

The Main Flow Component

code
// src/components/ThoughtRecordFlow.js
import React, { useReducer } from 'react';
import { initialState, thoughtRecordReducer } from '../hooks/useThoughtRecord';

// Import step components (we'll create one of these next)
import Step1_Situation from './steps/Step1_Situation';
import Step2_AutomaticThoughts from './steps/Step2_AutomaticThoughts';
// ... other step components

const ThoughtRecordFlow = () => {
  const [state, dispatch] = useReducer(thoughtRecordReducer, initialState);
  const { step, record } = state;

  const renderStep = () => {
    switch (step) {
      case 1:
        return <Step1_Situation record={record} dispatch={dispatch} />;
      case 2:
        return <Step2_AutomaticThoughts record={record} dispatch={dispatch} />;
      // ... cases for other steps
      default:
        return <Step1_Situation record={record} dispatch={dispatch} />;
    }
  };

  return (
    <div className: "cbt-flow-container">
      <h2>Thought Record - Step {step} of 7</h2>
      {renderStep()}
    </div>
  );
};

export default ThoughtRecordFlow;
Code collapsed

This component acts as our state manager and router for the form steps.

A Sample Step Component

Let's look at what Step1_Situation.js would look like.

code
// src/components/steps/Step1_Situation.js
import React from 'react';

const Step1_Situation = ({ record, dispatch }) => {
  const { situation } = record;

  const handleChange = (e) => {
    dispatch({
      type: 'UPDATE_FIELD',
      payload: { field: 'situation', value: e.target.value },
    });
  };

  const handleNext = () => {
    // Optional: Add validation here
    if (situation.trim()) {
      dispatch({ type: 'NEXT_STEP' });
    }
  };

  return (
    <div className: "step-container">
      <p>Describe the situation that triggered your difficult thought.</p>
      <textarea
        value={situation}
        onChange={handleChange}
        placeholder: "e.g., I was giving a presentation and my mind went blank."
      />
      <div className: "navigation-buttons">
        <button onClick={handleNext} disabled={!situation.trim()}>
          Next →
        </button>
      </div>
    </div>
  );
};

export default Step1_Situation;
Code collapsed

How It Works

  1. The component receives the current record state and the dispatch function as props.
  2. The textarea is a controlled component, with its value tied to record.situation.
  3. On every change, it dispatches an UPDATE_FIELD action.
  4. The "Next" button dispatches the NEXT_STEP action to advance the flow.

You would create similar components for each step of the thought record, each managing its own piece of the state via the centralized reducer.

Alternative Approach: Finite State Machines

Our useReducer implementation is excellent, but as flows get more complex (e.g., conditional steps, different user paths), it can be beneficial to use a finite state machine (FSM).

An FSM is a model where an application can be in only one of a finite number of states at any given time. A transition from one state to another is triggered by an event.

This perfectly describes our CBT exercise!

  • States: welcoming, writingSituation, identifyingThoughts, challenging, concluding.
  • Events: START_EXERCISE, NEXT, PREVIOUS, SAVE_DRAFT.

Libraries like XState allow you to define these flows declaratively.

code
// A conceptual XState machine definition
import { createMachine } from 'xstate';

const thoughtRecordMachine = createMachine({
  id: 'thoughtRecord',
  initial: 'situation',
  context: { /* our record data goes here */ },
  states: {
    situation: {
      on: { NEXT: 'automaticThoughts' }
    },
    automaticThoughts: {
      on: { NEXT: 'initialEmotions', PREVIOUS: 'situation' }
    },
    // ... more states
  }
});
Code collapsed

Why use a state machine?

  • Clarity: The entire user flow is defined in one place.
  • Safety: It prevents impossible state transitions (e.g., skipping a step).
  • Visualization: You can automatically generate visual diagrams of your user flows from the machine definition, which is amazing for documentation and team communication.

Security Best Practices

When building a mental health app, security and privacy are not optional features—they are the foundation of user trust. Mishandling sensitive data can have severe consequences.

  • Compliance: Be aware of regulations like HIPAA (in the US) and GDPR (in the EU). These have strict rules about handling protected health information (PHI).
  • Data Encryption: All data must be encrypted in transit (using HTTPS/TLS) and at rest (encrypting the database itself).
  • Authentication & Authorization: Use robust authentication methods (e.g., OAuth 2.0). Ensure a user can only ever access their own data.
  • Anonymity: Whenever possible, allow users to engage with features without providing unnecessary personally identifiable information (PII).
  • Transparent Privacy Policies: Clearly explain what data you collect and how it's used.

Conclusion

We've successfully broken down a complex therapeutic exercise into a manageable and scalable technical architecture. By starting with a clear data model, leveraging React's useReducer for centralized state management, and structuring our UI into discrete steps, we've built the foundation for a powerful and responsible CBT application.

You've learned to:

  • Model a real-world, multi-step process into a clean data structure.
  • Implement a robust multi-step form using useReducer.
  • Recognize when a finite state machine might be a better tool for the job.
  • Consider the critical security and privacy implications of building mental health tech.

The next step is to connect this frontend to a secure backend API, persist the data, and build out a dashboard for users to review their past thought records.

Resources

Frequently Asked Questions

Q: How do I handle partial progress if a user abandons a thought record mid-exercise?

A: Implement draft saving functionality that automatically persists state to localStorage or your backend on each step change. When the user returns, present their incomplete thought record with an option to continue or start fresh. This is crucial for therapeutic exercises where users may need to step away due to emotional intensity.

Q: Should I pre-fill examples or templates to help users get started with their first thought records?

A: Yes! First-time CBT users often find blank forms intimidating. Provide optional examples like "I'm worried about an upcoming presentation" with sample filled-in data. Make it clear these are just examples and their personal data remains private. Consider an interactive tutorial mode for first-time users.

Q: How do I handle emotional intensity scales that differ between users?

A: Allow customization of the intensity scale (1-5, 1-10, emoji-based) in user settings. Store the preference and use it consistently across all thought records. The key is consistency for each user's individual tracking rather than standardization across all users.

Q: Can I export thought records for users to share with their therapists, and how should I handle this data?

A: Implement a PDF export feature that formats thought records professionally for clinical review. Include only data the user explicitly selects for sharing. Consider adding a therapist share feature with time-limited access links and clear audit trails—this requires careful security implementation and explicit user consent.

Q: How do I handle users who want to revise past thought records after gaining new insights?

A: Allow editing of historical records but preserve the original as a version history. Show a "modified on" date when records have been updated. This is therapeutically valuable as users can see their growth over time. Consider implementing a "reflective revision" feature that prompts users to revisit particularly difficult thoughts after a set time period.

Related Articles

#

Article Tags

react
architecture
database
mentalhealth
webdev
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare Technology
Software Development
User Experience
AI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey