From 2b74770ae84173d76e2bcf86c34afd1e5f7ced55 Mon Sep 17 00:00:00 2001 From: Shawn Price Date: Thu, 16 Oct 2025 17:59:40 -0700 Subject: [PATCH] Initial config for LLM coding agents --- .claude/commands/code-review.md | 83 ++++ .claude/commands/plan-review.md | 118 +++++ .claude/commands/plan.md | 93 ++++ .gitignore | 7 +- .mcp.json.example | 8 + AGENTS.md | 783 ++++++++++++++++++++++++++++++++ CLAUDE.md | 783 ++++++++++++++++++++++++++++++++ 7 files changed, 1873 insertions(+), 2 deletions(-) create mode 100644 .claude/commands/code-review.md create mode 100644 .claude/commands/plan-review.md create mode 100644 .claude/commands/plan.md create mode 100644 .mcp.json.example create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/.claude/commands/code-review.md b/.claude/commands/code-review.md new file mode 100644 index 0000000..2d664c7 --- /dev/null +++ b/.claude/commands/code-review.md @@ -0,0 +1,83 @@ +# Code Review Command + +Please perform a code review given the following context about which branch to +compare to which other branch. There may be other context relevant to the +review. + +**IMPORTANT** Respond with a concise review that is useful to copy/past into a GitHub Pull Request comment. + +$ARGUMENTS + +**IMPORTANT** Follow the coding standards and best practices outlined in the +`.CLAUDE.md` file for this review. + +## Steps + +1. **List all changed files** + +Unless other specified in , get file changes by comparing +two branches. The may specifiy to get changes on the +current branch since a specific commit, in which case, modify the following +commands to do that. You know how. Regardless, you will end up with the +following two diff files to use. + +- Run + `git diff --name-only BASE_BRANCH...WORKING_BRANCH > tmp_diff_name_only.txt` + to get the exact list of files that changed. + +2. **Get the complete diff** + +- Run `git diff BASE_BRANCH...WORKING_BRANCH > tmp_full_diff.txt` to see all + actual changes. + +3. **Analyze each file thoroughly** + +- For every file in the diff: + - Read the full file content if it's a new/modified file to understand context + - Examine the specific changes line by line from the diff + - Check against project coding standards from CLAUDE.md + - All coding standards are important, but pay special attention to the + Frontend Rules and React coding styles and best practices. + - Identify potential issues: + - Security vulnerabilities or exposed sensitive data + - Performance bottlenecks or inefficiencies + - Logic bugs or edge cases not handled + - Code maintainability and readability concerns + - Missing error handling or validation + - Breaking changes that affect other parts of the codebase + - For each issue found, note the specific file path and line number references +- Assess the broader impact: how changes in each file affect related components, + APIs, database schemas, or user workflows + +4. **Create comprehensive review** + +- Write a complete and accurate code review document that covers: + - **Executive Summary**: Brief overview of changes, risk level, and key + concerns + - **Files Changed**: List all modified files with change summary + - **Critical Issues**: Security, breaking changes, or major bugs requiring + immediate attention + - **Detailed Analysis**: For each file with issues: + - `### path/to/file.ext` + - **Changes**: What was modified + - **Issues Found**: Specific problems with file:line references + - **Recommendations**: Actionable fixes with code examples where helpful + - **Impact**: How changes affect other parts of the system + - **Overall Assessment**: System-wide impact, testing recommendations, + deployment considerations + - **Action Items**: Prioritized checklist of required fixes and improvements + +5. **Save your review** + +- Save your full review to a new markdown file in the `.plans/` directory using + the format: `code-review-[BRANCH_NAME]-[TIMESTAMP_WITH_TIME].md` +- Include a brief summary at the top with the review date, branches compared, + and total files analyzed + +6. **Delete the temporary files** + +- Delete the temporary files created in steps 1 and 2: + - `tmp_diff_name_only.txt` + - `tmp_full_diff.txt` + +Be constructive and helpful in your feedback. diff --git a/.claude/commands/plan-review.md b/.claude/commands/plan-review.md new file mode 100644 index 0000000..a2256b5 --- /dev/null +++ b/.claude/commands/plan-review.md @@ -0,0 +1,118 @@ +# Plan Review Command + +Review an implementation plan for completeness, clarity, and feasibility by thoroughly analyzing both the plan document and the existing codebase it will integrate with. + +$ARGUMENTS + +## Overview + +This command reviews implementation plans by validating specifications AND reading the actual codebase to ensure the plan will integrate properly with existing code. + +## Steps + +1. **Load the plan document** + - Read the specified plan file mentioned in the . + +2. **Load and Validate the High Level Goals (Source of Truth)** + - Read the `## High level goals` section of the plan document carefully. + - **CRITICAL**: This section is the authoritative source of truth for what + must be accomplished. + - This section typically contains: + - Routes/pages/components affected + - Current State descriptions (what exists today) + - Required Changes (what needs to be built) + - Specific features and behaviors to implement + + **ESCAPE HATCH - Stop review if High Level Goals are inadequate:** + - If any of these issues are found, **STOP THE REVIEW** and report the + problem: + - Goals section is missing or empty + - Goals are vague or ambiguous (e.g., "improve performance" without + metrics) + - Internal contradictions within the goals themselves + - Current State and Required Changes are not clearly distinguished + - Goals mix implementation details with requirements + - Goals reference undefined components or systems + - When stopping early, provide: + - Specific examples of the problems found + - Clear guidance on how to improve the goals + - Offer to help rewrite the High Level Goals section + + - If goals pass validation: + - All implementation details in the rest of the plan must align with these + goals. + - If there's any conflict between the goals and other sections, the goals + take precedence. + +3. **Analyze plan structure and clarity** + - Verify all sections directly support the High Level Goals + - Ensure no implementation details contradict the stated goals + - Check for logical flow and organization + - Assess code-to-prose ratio (should be ~20% code examples, 80% + specifications) + - Evaluate whether architecture decisions are justified by the goals + +4. **Verify integration with existing code** + + **CRITICAL: Find and read all code files the plan references or will modify** + + - Extract all code references from the plan (functions, components, schemas, etc.) + - Use Grep/Glob to locate each referenced file + - Read the actual implementation to understand current structure + - Verify that assumed functions/components exist with compatible signatures + - Check that the plan follows existing patterns and conventions + - Identify any missing dependencies or utilities the plan requires + - Note any conflicts between plan assumptions and actual code + +5. **Evaluate completeness** + - Required sections present: goals, architecture, implementation steps, + testing + - State management strategy defined + - Error handling and edge cases addressed + - Performance considerations included + - Security implications considered + - Rollout/deployment strategy specified + +6. **Identify gaps and risks** + - Missing specifications or ambiguous requirements + - Technical blockers (missing dependencies, incompatible APIs) + - Undefined data flows or state transitions + - Absent testing or validation strategies + - Unaddressed scaling or performance limits + +7. **Generate comprehensive review** Create a review document with: + - **Rating**: Score out of 100 + - **Executive Summary**: 2-3 sentence overview + - **High Level Goals Alignment**: Confirm all goals from section 2 are + addressed + - **Code Integration Verification**: Summary of what was found vs. what the plan assumes + - **Strengths**: What the plan does well + - **Critical Gaps**: Blockers that prevent implementation + - **Missing Details**: Important but non-blocking omissions + - **Integration Issues**: Conflicts with existing code (be specific with file:line references) + - **Risk Assessment**: Potential failure points + - **Recommendations**: Specific improvements needed + - **Implementation Readiness**: Clear yes/no with justification + +8. **Score breakdown** Rate each aspect 0-100: + - High Level Goals alignment + - Architecture clarity + - Code integration (files exist, patterns match) + - Technical accuracy + - Risk mitigation + - Testing strategy + - Implementation readiness + +9. **Respond to the user** + - Present the review directly in the conversation + - Format for readability with clear sections and scores + - Highlight critical blockers that must be addressed + - Offer to: + - Save an improved version of the plan if score < 85 + - Discuss specific sections that need work + - Help refactor problematic areas + - Create implementation checklist if score ≥ 85 + - End with clear next steps or questions for the user + +Be constructive but thorough - a plan that scores below 85/100 needs significant +revision before implementation. diff --git a/.claude/commands/plan.md b/.claude/commands/plan.md new file mode 100644 index 0000000..2ac67ef --- /dev/null +++ b/.claude/commands/plan.md @@ -0,0 +1,93 @@ +You are an AI assistant acting as a senior-level software engineer. Your task is +to generate a comprehensive GitHub issue description for a given software +engineering task. This description should include a deep analysis of the +problem, its cause, and a detailed implementation guide. This is a planning +document and will be provided to a junior developer to implement. + +Here's the software engineering task you need to address: + + $ARGUMENTS + +Follow these steps to create the GitHub issue description: + +1. Research the problem: + + - Analyze the task description thoroughly + - Identify the key components and technologies involved + - Look for any potential challenges or complexities + - Consider any relevant best practices or design patterns + +2. Identify the cause of the problem: + + - Determine why this task is necessary + - Explore any existing limitations or issues that led to this task + - Consider any potential root causes or underlying system deficiencies + +3. Create a step-by-step implementation guide: + + - Break down the task into logical, manageable steps + - Provide clear and concise instructions for each step + - Consider potential edge cases and how to handle them + - Include code snippets only if necessary. This is a planning document and + will be provided to a developer to implement. + +4. Format the GitHub issue description using best practices: + + - Use a clear and concise title that summarizes the task + - Start with a brief overview of the problem + - Use markdown formatting for better readability (e.g., headers, lists, code + blocks) + - Include labels to categorize the issue (e.g., "enhancement", "bug", + "documentation") + - Add any relevant links or references + +5. Structure your GitHub issue description as follows: + - Title + - Problem Overview + - High Level Goals (this section is CRITICAL - see details below) + - Detailed Problem Analysis + - Root Cause + - Implementation Guide + - Additional Considerations + - Labels + +6. **High Level Goals Section Requirements**: + + This section must be placed near the top of the plan (after Problem Overview) + and serves as the authoritative source of truth for what must be accomplished. + + Include the following subsections: + + - **Routes/Pages/Components Affected**: List all specific files, routes, or + components that will be modified or created + - **Current State**: Clear description of what exists today in the codebase + for each affected area + - **Required Changes**: Specific features and behaviors that need to be + implemented, focusing on WHAT needs to be built, not HOW + - **Success Criteria**: Measurable outcomes that define when the task is + complete + + Guidelines for this section: + - Be specific and unambiguous - avoid vague goals like "improve performance" + without metrics + - Clearly distinguish between Current State and Required Changes + - Focus on requirements, not implementation details + - Ensure all goals are internally consistent with no contradictions + - Reference only components/systems that are defined elsewhere in the plan + - Make this section comprehensive enough that a developer could validate + their work against it + +Think through each of these steps carefully before composing your final GitHub +issue description. Use a section to organize your thoughts if +needed. + +Your final output should be the complete GitHub issue description, formatted +appropriately for GitHub. Include only the content that would appear in the +actual GitHub issue, without any additional commentary or explanations outside +of the issue description itself. + +Begin your response with the GitHub issue title in a single line, followed by +the full issue description. Use appropriate markdown formatting throughout. + +Output the description to the /.plans/ directory with a slug +representation of the title. diff --git a/.gitignore b/.gitignore index 797217f..810d8f0 100644 --- a/.gitignore +++ b/.gitignore @@ -42,9 +42,12 @@ yarn-error.log* next-env.d.ts # Claude Code -.claude +.claude/settings.local.json .plans .mcp.json # DB Backups -db-backups \ No newline at end of file +db-backups + +# Files +files \ No newline at end of file diff --git a/.mcp.json.example b/.mcp.json.example new file mode 100644 index 0000000..e2fc4ff --- /dev/null +++ b/.mcp.json.example @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "RepoPrompt": { + "command": "/Users/username/RepoPrompt/repoprompt_cli", + "args": [] + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bd294ee --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,783 @@ +# Agent Memory + +## Overview + +This application is an AI-powered Member of Parliament that analyzes and votes on bills before the current Canadian Parliament. It evaluates legislation against Build Canada's core tenets focused on economic prosperity, innovation, and government efficiency, providing reasoned judgments on whether to support, oppose, or abstain from each bill. + +## Frameworks & Technologies + +### Frontend +- **Next.js 15** (App Router with Turbopack) +- **React 19** with Server Components +- **Tailwind CSS 4** for styling +- **Shadcn UI** for components (`src/components/ui`) +- **React Markdown** with remark-gfm for rendering formatted content + +### Backend +- **Next.js API Routes** for server-side logic +- **MongoDB** with **Mongoose** ODM for bill storage and caching +- **NextAuth.js** for Google OAuth authentication +- **OpenAI GPT-5** for bill analysis and reasoning +- **Civics Project API** as the source of Canadian parliamentary bills + +## How Bill Analysis Works + +### High-Level Flow + +1. Bill Retrieval +2. Text Conversion +3. AI Analysis +4. Social Issue Classification +5. Database Caching +6. UI Display + +### Detailed Technical Flow + +#### 1. Bill Retrieval (`src/app/[id]/page.tsx`) + +When a user requests a bill (e.g., C-18, S-5): + +- **Primary source**: Check MongoDB database for cached analysis (`src/server/get-bill-by-id-from-db.ts`) +- **Fallback source**: If not in database, fetch from Civics Project API (`getBillFromCivicsProjectApi()` in `src/services/billApi.ts`) + - Endpoint: `https://api.civicsproject.org/bills/canada/{billId}/45` + - Returns bill metadata, stages, sponsors, and source URL for full text +- The retrieval logic is orchestrated in the bill detail page component, not through a unified service layer + +#### 2. XML to Markdown Conversion (`src/utils/xml-to-md/xml-to-md.util.ts`) + +Bill text is downloaded as XML from the official Canadian Parliament source: + +- Uses `fast-xml-parser` to parse structured bill XML +- Converts XML structure to clean Markdown: + - `` Document header with bill number and long title + - `
` H3 headings with labels + - `` Bullet points + - `` Markdown italics/bold + - Preserves legal structure (sections, subsections, provisions) + +#### 3. AI Analysis with Structured Reasoning (`summarizeBillText()` in `src/services/billApi.ts`) + +The Markdown bill text is sent to OpenAI's GPT-5 model with high reasoning effort using the Responses API: + +**Input**: Structured prompt (`src/prompt/summary-and-vote-prompt.ts`) combined with bill text + +**Model configuration**: +- Model: `gpt-5` +- API method: `OpenAI.responses.create()` +- Reasoning effort: `high` + +**Output**: Structured JSON containing: +- `summary`: 3-5 sentence plain-language explanation +- `short_title`: Concise bill name (1-2 words) +- `tenet_evaluations`: Array of 8 evaluations (aligns/conflicts/neutral) with explanations +- `final_judgment`: "yes" (support), "no" (oppose), or "abstain" +- `rationale`: 2 sentence explanation + bullet points for the judgment +- `steel_man`: Best possible interpretation of the bill +- `question_period_questions`: 3 critical questions for parliamentary debate +- `needs_more_info`: Flag for insufficient information +- `missing_details`: Array of information gaps + +**Fallback behavior**: If OpenAI API key is missing or analysis fails, returns neutral stance with all tenet evaluations marked as neutral and appropriate error messages + +#### 4. Social Issue Classification (`src/services/social-issue-grader.ts`) + +A separate AI classifier determines if the bill is primarily a social issue: + +**Social issues include**: +- Recognition/commemoration (heritage days, national symbols) +- Rights & identity (assisted dying, gender identity, indigenous rights) +- Culture & language (multiculturalism, official languages) +- Civil liberties & expression (protests, speech regulations) + +**Not social issues**: +- Core economics (budgets, taxation, trade) +- Infrastructure operations (transportation, energy, housing) +- Technical/administrative procedures + +**Classification timing**: Only runs for new bills or existing bills missing the `isSocialIssue` field to avoid redundant AI calls + +**Classification result**: +- Returns boolean value stored as `isSocialIssue` field +- Used by the UI to determine whether to display the analysis and judgment +- Build Canada focuses on economic policy and abstains from social/cultural legislation + +#### 5. Bill Caching (`src/utils/billConverters.ts`) + +To avoid redundant AI calls and reduce costs, the system implements smart caching: + +**When fetching from API** (`fromCivicsProjectApiBill()`): +1. **Check existing analysis**: Query MongoDB for bill by ID +2. **Source comparison**: Compare `bill.source` URL from API with cached version +3. **Conditional re-analysis**: + - If source URL unchanged - Use cached analysis (skip AI call) + - If source URL changed - Fetch new XML, regenerate full analysis + - Social issue classification only runs if missing from database + +**When updating database** (`onBillNotInDatabase()`): +1. Checks if bill already exists in database +2. Updates if any of these conditions are true: + - Source URL changed + - Bill texts count changed + - Question Period questions missing or different + - Short title missing +3. Creates new database entry if bill doesn't exist + +#### 6. Database Storage + +Analyzed bills are stored in MongoDB using the `Bill` schema (`src/models/Bill.ts`). + +Users are stored in MongoDB using the `User` schema (`src/models/User.ts`). + +#### 7. Authentication and Bill Editing (`src/lib/auth.ts`) + +The application uses NextAuth.js with Google OAuth for authentication to protect bill editing functionality: + +**Authentication Flow**: +1. **Provider**: Google OAuth configured with email and profile scopes +2. **Session strategy**: JWT-based sessions (no database sessions) +3. **User allowlist**: Database-backed allowlist using the `User` model (`src/models/User.ts`) + - Users must exist in the database with `allowed: true` field + - Sign-in callback checks user existence and `allowed` status + - No auto-creation of users on sign-in +4. **Session updates**: Updates `lastLoginAt` timestamp on each successful sign-in + +**Where authentication is used**: +- **Bill editing page** (`src/app/[id]/edit/page.tsx`): Uses `requireAuthenticatedUser()` guard +- **Bill update API** (`src/app/api/[id]/route.ts`): Checks session and user database record +- **Auth guard** (`src/lib/auth-guards.ts`): Reusable server-side authentication helper that redirects to `/unauthorized` if not authenticated + +**Protected functionality**: +- Editing bill metadata (title, short_title, summary) +- Modifying AI analysis results (final_judgment, rationale, steel_man) +- Updating tenet evaluations +- Managing Question Period questions +- Editing genres and missing details + +Only users with valid Google accounts that exist in the database with `allowed: true` can edit bills. + +### Optimization Features + +1. **Page-level caching**: Bill detail pages revalidate every 120 seconds in production + - API requests cache: 1 hour for bill metadata (`BILL_API_REVALIDATE_INTERVAL`) + - XML bill text cache: 1 hour for bill full text + - Development: No caching for immediate feedback + +2. **Fallback handling**: If AI fails, returns neutral stance with all tenet evaluations marked neutral and appropriate error messages + +3. **Smart classification**: Social issue classification only runs once per bill, skipped if already classified in database + +4. **Smart updates**: Database updates only occur when: + - Source URL changes (triggers re-analysis) + - Bill texts count changes + - Question Period questions are missing or updated + - Short title is missing + +## Key Files Reference + +### API & Data Fetching +- **`src/services/billApi.ts`** - Core API integration and AI analysis +- **`src/utils/billConverters.ts`** - Data transformation and caching logic +- **`src/utils/xml-to-md/xml-to-md.util.ts`** - XML parsing and Markdown conversion + +### AI & Analysis +- **`src/prompt/summary-and-vote-prompt.ts`** - AI prompt with Build Canada tenets +- **`src/services/social-issue-grader.ts`** - Social issue classification + +### Database +- **`src/models/Bill.ts`** - MongoDB schema definition for bills +- **`src/models/User.ts`** - MongoDB schema definition for users (authentication allowlist) +- **`src/server/get-unified-bill-by-id.ts`** - Helper to convert DB bill to unified format +- **`src/server/get-bill-by-id-from-db.ts`** - Database query for retrieving bills +- **`src/server/get-all-bills-from-db.ts`** - Database query for retrieving all bills + +### Authentication +- **`src/lib/auth.ts`** - NextAuth.js configuration with Google OAuth +- **`src/lib/auth-guards.ts`** - Reusable server-side authentication guards +- **`src/app/api/auth/[...nextauth]/route.ts`** - NextAuth.js API route handler +- **`src/app/api/[id]/route.ts`** - Bill update API endpoint (protected) + +### UI Components +- **`src/app/page.tsx`** - Home page with bill list +- **`src/app/BillExplorer.tsx`** - Client component for filtering and searching bills +- **`src/app/[id]/page.tsx`** - Bill detail page (Server Component) +- **`src/components/BillDetail/BillAnalysis.tsx`** - Renders tenet evaluations and judgment +- **`src/components/BillDetail/BillHeader.tsx`** - Displays bill title and status +- **`src/components/BillDetail/BillTenets.tsx`** - Displays Build Canada tenets section + +### Supporting Files +- **`src/lib/mongoose.ts`** - MongoDB connection management +- **`src/utils/should-show-determination/should-show-determination.util.ts`** - Logic for determining when to display analysis + +## Environment Variables + +See `.env.example` for the required environment variables. + +## Available Commands For Development + +- `pnpm run check` - Run type checking, formatting, and linting. +- `pnpm run test` - Run tests. +- `pnpm run type-check` - Run type checking. +- `pnpm run lint` - Run linting. + +**IMPORTANT**: Do not run `pnpm run build` or `npm run build` + +## Coding Rules + +### Frontend Rules + +Follow these rules when working on the frontend. + +It uses Next.js, React, Tailwind, and Shadcn. + +#### General Rules + +- Use `lucide-react` for icons +- Use Tailwind CSS classes for all colors, spacing, and typography +- For color, use Tailwind CSS classes. + +#### Components + +- Use divs instead of other html tags unless otherwise specified +- Separate the main parts of a component's html with an extra blank line for + visual spacing +- Only use `"use client"` directive when component needs client-side interactivity (state, events, hooks) +- Server components (default) don't need any directive + +##### Organization + +- All components be named using capital case like `ExampleComponent.tsx` unless + otherwise specified +- Components in `src/components` use descriptive names (e.g., `BillCard.tsx`, `BillAnalysis.tsx`) +- Utility components use kebab-case with `.component.tsx` suffix (e.g., `filter-section.component.tsx`, `nav.component.tsx`) + +##### Data Fetching + +- Fetch data in server components and pass the data down as props to client + components +- Import helper functions from `src/server/` for database queries (e.g., `getBillByIdFromDB`, `getAllBillsFromDB`) +- For authentication, use `getServerSession` from `next-auth` in server components + +##### Server Pages & Components + +- **Do NOT use `"use server"` directive** - it's not needed for server components/pages in Next.js App Router +- Server components/pages are the default; they don't need any directive +- Async pages and components directly await data without Suspense in most cases +- Route params must be awaited: `const { id } = await params` where type is `params: Promise<{ id: string }>` +- Server components cannot be imported into client components - pass them as props via the `children` pattern + +Example of a server layout (no directive needed): + +```tsx +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Page Title", + description: "Page description", +}; + +export default async function ExampleLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +} +``` + +Example of a server page with data fetching (no directive, no Suspense): + +```tsx +import { getBillByIdFromDB } from "@/server/get-bill-by-id-from-db"; +import { BillHeader } from "@/components/BillDetail/BillHeader"; + +interface Params { + params: Promise<{ id: string }>; +} + +export default async function BillDetailPage({ params }: Params) { + const { id } = await params; + + // Fetch data directly in the component + const bill = await getBillByIdFromDB(id); + + if (!bill) { + return
Bill not found
; + } + + return ( +
+ + {/* ... rest of UI */} +
+ ); +} +``` + +Example of a reusable server component (no directive): + +```tsx +import type { UnifiedBill } from "@/utils/billConverters"; + +interface BillHeaderProps { + bill: UnifiedBill; +} + +export function BillHeader({ bill }: BillHeaderProps) { + return ( +
+

{bill.short_title}

+

{bill.title}

+
+ ); +} +``` + +##### Client Components + +- Use `"use client"` directive at the top of the file +- Client components handle interactivity (state, events, hooks) +- Receive data as props from server components +- Use React hooks: `useState`, `useEffect`, `useMemo`, `useCallback` +- For authentication state, use `useSession()` from `next-auth/react` + +Example of a client component with state: + +```tsx +"use client"; + +import { useState } from "react"; +import { BillSummary } from "@/app/types"; +import BillCard from "@/components/BillCard"; + +interface BillExplorerProps { + bills: BillSummary[]; +} + +export default function BillExplorer({ bills }: BillExplorerProps) { + const [search, setSearch] = useState(""); + + const filteredBills = bills.filter((bill) => + bill.title.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+ setSearch(e.target.value)} + placeholder="Search bills..." + /> + +
    + {filteredBills.map((bill) => ( + + ))} +
+
+ ); +} +``` + +Example of a simple client component (no state): + +```tsx +"use client"; + +import Link from "next/link"; +import { memo } from "react"; +import { BillSummary } from "@/app/types"; + +interface BillCardProps { + bill: BillSummary; +} + +function BillCard({ bill }: BillCardProps) { + return ( +
  • + +

    {bill.shortTitle ?? bill.title}

    +

    {bill.description}

    + +
  • + ); +} + +export default memo(BillCard); +``` + +#### When and How to Use `useEffect` in React + +##### When NOT to Use `useEffect` + +• **Transforming Data for Rendering:** Calculate derived data (e.g., filtered +lists, computed values) directly during rendering, not in Effects or extra +state. + +```tsx +// 🔴 Bad: Unnecessary Effect for calculation +function Form() { + const [firstName, setFirstName] = useState("Taylor") + const [lastName, setLastName] = useState("Swift") + const [fullName, setFullName] = useState("") + + useEffect(() => { + setFullName(firstName + " " + lastName) + }, [firstName, lastName]) + // ... +} + +// ✅ Good: Calculate during rendering +function Form() { + const [firstName, setFirstName] = useState("Taylor") + const [lastName, setLastName] = useState("Swift") + const fullName = firstName + " " + lastName + // ... +} +``` + +• **Handling User Events:** Place logic for user actions (e.g., POST requests, +notifications) in event handlers, not Effects. + +```tsx +// 🔴 Bad: Event-specific logic inside an Effect +function ProductPage({ product, addToCart }) { + useEffect(() => { + if (product.isInCart) { + showNotification(`Added ${product.name} to the cart!`) + } + }, [product]) + + function handleBuyClick() { + addToCart(product) + } + // ... +} + +// ✅ Good: Event-specific logic in event handler +function ProductPage({ product, addToCart }) { + function handleBuyClick() { + addToCart(product) + showNotification(`Added ${product.name} to the cart!`) + } + // ... +} +``` + +• **Updating State Based on Props/State:** If a value can be derived from props +or state, compute it during render. Use `useMemo` only for expensive +calculations. + +```tsx +// 🔴 Bad: Redundant state and unnecessary Effect +function TodoList({ todos, filter }) { + const [visibleTodos, setVisibleTodos] = useState([]) + + useEffect(() => { + setVisibleTodos(getFilteredTodos(todos, filter)) + }, [todos, filter]) + // ... +} + +// ✅ Good: Calculate during rendering +function TodoList({ todos, filter }) { + const visibleTodos = getFilteredTodos(todos, filter) + // ... +} + +// ✅ Good: Use useMemo for expensive calculations +function TodoList({ todos, filter }) { + const visibleTodos = useMemo( + () => getFilteredTodos(todos, filter), + [todos, filter], + ) + // ... +} +``` + +• **Resetting State on Prop Change:** To reset all state when a prop changes, +use the `key` prop on the component. For partial resets, adjust state during +render or, preferably, lift state up. + +```tsx +// 🔴 Bad: Resetting state on prop change in an Effect +export default function ProfilePage({ userId }) { + const [comment, setComment] = useState("") + + useEffect(() => { + setComment("") + }, [userId]) + // ... +} + +// ✅ Good: Use key prop to reset state +export default function ProfilePage({ userId }) { + return +} + +function Profile({ userId }) { + const [comment, setComment] = useState("") + // State will reset automatically when key changes + // ... +} +``` + +• **Sharing Logic Between Event Handlers:** Extract shared logic into a function +called by each handler, not into an Effect. + +• **Notifying Parent Components:** Call parent callbacks directly in event +handlers, not in Effects. Or, lift state up to the parent. + +```tsx +// 🔴 Bad: Notifying parent in an Effect +function Toggle({ onChange }) { + const [isOn, setIsOn] = useState(false) + + useEffect(() => { + onChange(isOn) + }, [isOn, onChange]) + + function handleClick() { + setIsOn(!isOn) + } + // ... +} + +// ✅ Good: Notify parent in event handler +function Toggle({ onChange }) { + const [isOn, setIsOn] = useState(false) + + function handleClick() { + const nextIsOn = !isOn + setIsOn(nextIsOn) + onChange(nextIsOn) + } + // ... +} +``` + +• **Passing Data to Parent:** Fetch data in the parent and pass it down as +props, rather than having children update parent state via Effects. + +• **Chaining State Updates Solely to Trigger Effects:** Avoid chains of Effects +that update state to trigger other Effects. Instead, calculate during render and +update state in event handlers. + +```tsx +// 🔴 Bad: Chains of Effects +function Game() { + const [card, setCard] = useState(null) + const [goldCardCount, setGoldCardCount] = useState(0) + const [round, setRound] = useState(1) + + useEffect(() => { + if (card !== null && card.gold) { + setGoldCardCount((c) => c + 1) + } + }, [card]) + + useEffect(() => { + if (goldCardCount > 3) { + setRound((r) => r + 1) + setGoldCardCount(0) + } + }, [goldCardCount]) + // ... +} + +// ✅ Good: Calculate in event handler +function Game() { + const [card, setCard] = useState(null) + const [goldCardCount, setGoldCardCount] = useState(0) + const [round, setRound] = useState(1) + + function handlePlaceCard(nextCard) { + setCard(nextCard) + if (nextCard.gold) { + if (goldCardCount <= 3) { + setGoldCardCount(goldCardCount + 1) + } else { + setGoldCardCount(0) + setRound(round + 1) + } + } + } + // ... +} +``` + +• **App Initialization Logic:** For logic that should run once per app load (not +per mount), use a top-level flag or module-level code, not an Effect. + +```tsx +// 🔴 Bad: Effect that should only run once +function App() { + useEffect(() => { + loadDataFromLocalStorage() + checkAuthToken() + }, []) + // ... +} + +// ✅ Good: Use a flag or module-level code +let didInit = false + +function App() { + useEffect(() => { + if (!didInit) { + didInit = true + loadDataFromLocalStorage() + checkAuthToken() + } + }, []) + // ... +} + +// ✅ Better: Run at module level +if (typeof window !== "undefined") { + checkAuthToken() + loadDataFromLocalStorage() +} + +function App() { + // ... +} +``` + +##### When to Use `useEffect` + +• **Synchronizing with External Systems:** Use Effects to sync with non-React +systems (e.g., browser APIs, third-party widgets, subscriptions). + +```tsx +// ✅ Good: Subscribing to browser events +function useOnlineStatus() { + const [isOnline, setIsOnline] = useState(navigator.onLine) + + useEffect(() => { + function updateState() { + setIsOnline(navigator.onLine) + } + + window.addEventListener("online", updateState) + window.addEventListener("offline", updateState) + + return () => { + window.removeEventListener("online", updateState) + window.removeEventListener("offline", updateState) + } + }, []) + + return isOnline +} +``` + +• **Fetching Data:** Use Effects to fetch data that should stay in sync with +props/state. Always implement cleanup to avoid race conditions. + +```tsx +// ✅ Good: Data fetching with cleanup +function SearchResults({ query }) { + const [results, setResults] = useState([]) + + useEffect(() => { + let ignore = false + + fetchResults(query).then((json) => { + if (!ignore) { + setResults(json) + } + }) + + return () => { + ignore = true + } + }, [query]) + + return +} +``` + +• **Subscribing to External Stores:** Use Effects (or preferably +`useSyncExternalStore`) to subscribe to external data sources. + +```tsx +// ✅ Better: Using useSyncExternalStore +function useOnlineStatus() { + return useSyncExternalStore( + subscribe, + () => navigator.onLine, + () => true, + ) +} + +function subscribe(callback) { + window.addEventListener("online", callback) + window.addEventListener("offline", callback) + return () => { + window.removeEventListener("online", callback) + window.removeEventListener("offline", callback) + } +} +``` + +• **Logic Triggered by Component Display:** Use Effects for logic that should +run because the component was displayed (e.g., analytics events on mount). + +```tsx +// ✅ Good: Analytics on mount +function Form() { + useEffect(() => { + post("/analytics/event", { eventName: "visit_form" }) + }, []) + + // Form logic... +} +``` + +##### Best Practices & Alternatives + +• **Calculate Derived State During Render:** Don't store redundant state; +compute from existing state/props. + +• **Use `useMemo` for Expensive Calculations:** Only memoize if the calculation +is slow and depends on specific values. + +• **Use the `key` Prop to Reset State:** Changing the `key` will remount the +component and reset its state. + +• **Lift State Up:** If multiple components need to stay in sync, move state to +their common ancestor. + +• **Handle User Interactions in Event Handlers:** All logic tied to user actions +should be in event handlers, not Effects. + +• **Extract Reusable Logic:** Move shared logic into functions or custom hooks, +not Effects. + +• **App-Wide Initialization:** Use a module-level flag or code for logic that +must run once per app load. + +• **Use `useSyncExternalStore` for Subscriptions:** Prefer this over manual +Effect-based subscriptions for external data. + +• **Cleanup in Data Fetching Effects:** Always add cleanup to ignore stale +responses and prevent race conditions. + +• **Custom Hooks for Complex Effects:** Abstract complex Effect logic (like data +fetching) into custom hooks for clarity and reuse. + +• **Prefer Framework Data Fetching:** Use your framework's built-in data +fetching when possible for better performance and ergonomics. + +##### Summary + +Use `useEffect` only for synchronizing with external systems or when a side +effect is required because a component is displayed. For everything +else—especially data transformation, user events, and derived state—prefer +direct calculation, event handlers, or lifting state up. This leads to simpler, +faster, and more maintainable React code. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bd294ee --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,783 @@ +# Agent Memory + +## Overview + +This application is an AI-powered Member of Parliament that analyzes and votes on bills before the current Canadian Parliament. It evaluates legislation against Build Canada's core tenets focused on economic prosperity, innovation, and government efficiency, providing reasoned judgments on whether to support, oppose, or abstain from each bill. + +## Frameworks & Technologies + +### Frontend +- **Next.js 15** (App Router with Turbopack) +- **React 19** with Server Components +- **Tailwind CSS 4** for styling +- **Shadcn UI** for components (`src/components/ui`) +- **React Markdown** with remark-gfm for rendering formatted content + +### Backend +- **Next.js API Routes** for server-side logic +- **MongoDB** with **Mongoose** ODM for bill storage and caching +- **NextAuth.js** for Google OAuth authentication +- **OpenAI GPT-5** for bill analysis and reasoning +- **Civics Project API** as the source of Canadian parliamentary bills + +## How Bill Analysis Works + +### High-Level Flow + +1. Bill Retrieval +2. Text Conversion +3. AI Analysis +4. Social Issue Classification +5. Database Caching +6. UI Display + +### Detailed Technical Flow + +#### 1. Bill Retrieval (`src/app/[id]/page.tsx`) + +When a user requests a bill (e.g., C-18, S-5): + +- **Primary source**: Check MongoDB database for cached analysis (`src/server/get-bill-by-id-from-db.ts`) +- **Fallback source**: If not in database, fetch from Civics Project API (`getBillFromCivicsProjectApi()` in `src/services/billApi.ts`) + - Endpoint: `https://api.civicsproject.org/bills/canada/{billId}/45` + - Returns bill metadata, stages, sponsors, and source URL for full text +- The retrieval logic is orchestrated in the bill detail page component, not through a unified service layer + +#### 2. XML to Markdown Conversion (`src/utils/xml-to-md/xml-to-md.util.ts`) + +Bill text is downloaded as XML from the official Canadian Parliament source: + +- Uses `fast-xml-parser` to parse structured bill XML +- Converts XML structure to clean Markdown: + - `` Document header with bill number and long title + - `
    ` H3 headings with labels + - `` Bullet points + - `` Markdown italics/bold + - Preserves legal structure (sections, subsections, provisions) + +#### 3. AI Analysis with Structured Reasoning (`summarizeBillText()` in `src/services/billApi.ts`) + +The Markdown bill text is sent to OpenAI's GPT-5 model with high reasoning effort using the Responses API: + +**Input**: Structured prompt (`src/prompt/summary-and-vote-prompt.ts`) combined with bill text + +**Model configuration**: +- Model: `gpt-5` +- API method: `OpenAI.responses.create()` +- Reasoning effort: `high` + +**Output**: Structured JSON containing: +- `summary`: 3-5 sentence plain-language explanation +- `short_title`: Concise bill name (1-2 words) +- `tenet_evaluations`: Array of 8 evaluations (aligns/conflicts/neutral) with explanations +- `final_judgment`: "yes" (support), "no" (oppose), or "abstain" +- `rationale`: 2 sentence explanation + bullet points for the judgment +- `steel_man`: Best possible interpretation of the bill +- `question_period_questions`: 3 critical questions for parliamentary debate +- `needs_more_info`: Flag for insufficient information +- `missing_details`: Array of information gaps + +**Fallback behavior**: If OpenAI API key is missing or analysis fails, returns neutral stance with all tenet evaluations marked as neutral and appropriate error messages + +#### 4. Social Issue Classification (`src/services/social-issue-grader.ts`) + +A separate AI classifier determines if the bill is primarily a social issue: + +**Social issues include**: +- Recognition/commemoration (heritage days, national symbols) +- Rights & identity (assisted dying, gender identity, indigenous rights) +- Culture & language (multiculturalism, official languages) +- Civil liberties & expression (protests, speech regulations) + +**Not social issues**: +- Core economics (budgets, taxation, trade) +- Infrastructure operations (transportation, energy, housing) +- Technical/administrative procedures + +**Classification timing**: Only runs for new bills or existing bills missing the `isSocialIssue` field to avoid redundant AI calls + +**Classification result**: +- Returns boolean value stored as `isSocialIssue` field +- Used by the UI to determine whether to display the analysis and judgment +- Build Canada focuses on economic policy and abstains from social/cultural legislation + +#### 5. Bill Caching (`src/utils/billConverters.ts`) + +To avoid redundant AI calls and reduce costs, the system implements smart caching: + +**When fetching from API** (`fromCivicsProjectApiBill()`): +1. **Check existing analysis**: Query MongoDB for bill by ID +2. **Source comparison**: Compare `bill.source` URL from API with cached version +3. **Conditional re-analysis**: + - If source URL unchanged - Use cached analysis (skip AI call) + - If source URL changed - Fetch new XML, regenerate full analysis + - Social issue classification only runs if missing from database + +**When updating database** (`onBillNotInDatabase()`): +1. Checks if bill already exists in database +2. Updates if any of these conditions are true: + - Source URL changed + - Bill texts count changed + - Question Period questions missing or different + - Short title missing +3. Creates new database entry if bill doesn't exist + +#### 6. Database Storage + +Analyzed bills are stored in MongoDB using the `Bill` schema (`src/models/Bill.ts`). + +Users are stored in MongoDB using the `User` schema (`src/models/User.ts`). + +#### 7. Authentication and Bill Editing (`src/lib/auth.ts`) + +The application uses NextAuth.js with Google OAuth for authentication to protect bill editing functionality: + +**Authentication Flow**: +1. **Provider**: Google OAuth configured with email and profile scopes +2. **Session strategy**: JWT-based sessions (no database sessions) +3. **User allowlist**: Database-backed allowlist using the `User` model (`src/models/User.ts`) + - Users must exist in the database with `allowed: true` field + - Sign-in callback checks user existence and `allowed` status + - No auto-creation of users on sign-in +4. **Session updates**: Updates `lastLoginAt` timestamp on each successful sign-in + +**Where authentication is used**: +- **Bill editing page** (`src/app/[id]/edit/page.tsx`): Uses `requireAuthenticatedUser()` guard +- **Bill update API** (`src/app/api/[id]/route.ts`): Checks session and user database record +- **Auth guard** (`src/lib/auth-guards.ts`): Reusable server-side authentication helper that redirects to `/unauthorized` if not authenticated + +**Protected functionality**: +- Editing bill metadata (title, short_title, summary) +- Modifying AI analysis results (final_judgment, rationale, steel_man) +- Updating tenet evaluations +- Managing Question Period questions +- Editing genres and missing details + +Only users with valid Google accounts that exist in the database with `allowed: true` can edit bills. + +### Optimization Features + +1. **Page-level caching**: Bill detail pages revalidate every 120 seconds in production + - API requests cache: 1 hour for bill metadata (`BILL_API_REVALIDATE_INTERVAL`) + - XML bill text cache: 1 hour for bill full text + - Development: No caching for immediate feedback + +2. **Fallback handling**: If AI fails, returns neutral stance with all tenet evaluations marked neutral and appropriate error messages + +3. **Smart classification**: Social issue classification only runs once per bill, skipped if already classified in database + +4. **Smart updates**: Database updates only occur when: + - Source URL changes (triggers re-analysis) + - Bill texts count changes + - Question Period questions are missing or updated + - Short title is missing + +## Key Files Reference + +### API & Data Fetching +- **`src/services/billApi.ts`** - Core API integration and AI analysis +- **`src/utils/billConverters.ts`** - Data transformation and caching logic +- **`src/utils/xml-to-md/xml-to-md.util.ts`** - XML parsing and Markdown conversion + +### AI & Analysis +- **`src/prompt/summary-and-vote-prompt.ts`** - AI prompt with Build Canada tenets +- **`src/services/social-issue-grader.ts`** - Social issue classification + +### Database +- **`src/models/Bill.ts`** - MongoDB schema definition for bills +- **`src/models/User.ts`** - MongoDB schema definition for users (authentication allowlist) +- **`src/server/get-unified-bill-by-id.ts`** - Helper to convert DB bill to unified format +- **`src/server/get-bill-by-id-from-db.ts`** - Database query for retrieving bills +- **`src/server/get-all-bills-from-db.ts`** - Database query for retrieving all bills + +### Authentication +- **`src/lib/auth.ts`** - NextAuth.js configuration with Google OAuth +- **`src/lib/auth-guards.ts`** - Reusable server-side authentication guards +- **`src/app/api/auth/[...nextauth]/route.ts`** - NextAuth.js API route handler +- **`src/app/api/[id]/route.ts`** - Bill update API endpoint (protected) + +### UI Components +- **`src/app/page.tsx`** - Home page with bill list +- **`src/app/BillExplorer.tsx`** - Client component for filtering and searching bills +- **`src/app/[id]/page.tsx`** - Bill detail page (Server Component) +- **`src/components/BillDetail/BillAnalysis.tsx`** - Renders tenet evaluations and judgment +- **`src/components/BillDetail/BillHeader.tsx`** - Displays bill title and status +- **`src/components/BillDetail/BillTenets.tsx`** - Displays Build Canada tenets section + +### Supporting Files +- **`src/lib/mongoose.ts`** - MongoDB connection management +- **`src/utils/should-show-determination/should-show-determination.util.ts`** - Logic for determining when to display analysis + +## Environment Variables + +See `.env.example` for the required environment variables. + +## Available Commands For Development + +- `pnpm run check` - Run type checking, formatting, and linting. +- `pnpm run test` - Run tests. +- `pnpm run type-check` - Run type checking. +- `pnpm run lint` - Run linting. + +**IMPORTANT**: Do not run `pnpm run build` or `npm run build` + +## Coding Rules + +### Frontend Rules + +Follow these rules when working on the frontend. + +It uses Next.js, React, Tailwind, and Shadcn. + +#### General Rules + +- Use `lucide-react` for icons +- Use Tailwind CSS classes for all colors, spacing, and typography +- For color, use Tailwind CSS classes. + +#### Components + +- Use divs instead of other html tags unless otherwise specified +- Separate the main parts of a component's html with an extra blank line for + visual spacing +- Only use `"use client"` directive when component needs client-side interactivity (state, events, hooks) +- Server components (default) don't need any directive + +##### Organization + +- All components be named using capital case like `ExampleComponent.tsx` unless + otherwise specified +- Components in `src/components` use descriptive names (e.g., `BillCard.tsx`, `BillAnalysis.tsx`) +- Utility components use kebab-case with `.component.tsx` suffix (e.g., `filter-section.component.tsx`, `nav.component.tsx`) + +##### Data Fetching + +- Fetch data in server components and pass the data down as props to client + components +- Import helper functions from `src/server/` for database queries (e.g., `getBillByIdFromDB`, `getAllBillsFromDB`) +- For authentication, use `getServerSession` from `next-auth` in server components + +##### Server Pages & Components + +- **Do NOT use `"use server"` directive** - it's not needed for server components/pages in Next.js App Router +- Server components/pages are the default; they don't need any directive +- Async pages and components directly await data without Suspense in most cases +- Route params must be awaited: `const { id } = await params` where type is `params: Promise<{ id: string }>` +- Server components cannot be imported into client components - pass them as props via the `children` pattern + +Example of a server layout (no directive needed): + +```tsx +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Page Title", + description: "Page description", +}; + +export default async function ExampleLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
    + {children} +
    + ); +} +``` + +Example of a server page with data fetching (no directive, no Suspense): + +```tsx +import { getBillByIdFromDB } from "@/server/get-bill-by-id-from-db"; +import { BillHeader } from "@/components/BillDetail/BillHeader"; + +interface Params { + params: Promise<{ id: string }>; +} + +export default async function BillDetailPage({ params }: Params) { + const { id } = await params; + + // Fetch data directly in the component + const bill = await getBillByIdFromDB(id); + + if (!bill) { + return
    Bill not found
    ; + } + + return ( +
    + + {/* ... rest of UI */} +
    + ); +} +``` + +Example of a reusable server component (no directive): + +```tsx +import type { UnifiedBill } from "@/utils/billConverters"; + +interface BillHeaderProps { + bill: UnifiedBill; +} + +export function BillHeader({ bill }: BillHeaderProps) { + return ( +
    +

    {bill.short_title}

    +

    {bill.title}

    +
    + ); +} +``` + +##### Client Components + +- Use `"use client"` directive at the top of the file +- Client components handle interactivity (state, events, hooks) +- Receive data as props from server components +- Use React hooks: `useState`, `useEffect`, `useMemo`, `useCallback` +- For authentication state, use `useSession()` from `next-auth/react` + +Example of a client component with state: + +```tsx +"use client"; + +import { useState } from "react"; +import { BillSummary } from "@/app/types"; +import BillCard from "@/components/BillCard"; + +interface BillExplorerProps { + bills: BillSummary[]; +} + +export default function BillExplorer({ bills }: BillExplorerProps) { + const [search, setSearch] = useState(""); + + const filteredBills = bills.filter((bill) => + bill.title.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
    + setSearch(e.target.value)} + placeholder="Search bills..." + /> + +
      + {filteredBills.map((bill) => ( + + ))} +
    +
    + ); +} +``` + +Example of a simple client component (no state): + +```tsx +"use client"; + +import Link from "next/link"; +import { memo } from "react"; +import { BillSummary } from "@/app/types"; + +interface BillCardProps { + bill: BillSummary; +} + +function BillCard({ bill }: BillCardProps) { + return ( +
  • + +

    {bill.shortTitle ?? bill.title}

    +

    {bill.description}

    + +
  • + ); +} + +export default memo(BillCard); +``` + +#### When and How to Use `useEffect` in React + +##### When NOT to Use `useEffect` + +• **Transforming Data for Rendering:** Calculate derived data (e.g., filtered +lists, computed values) directly during rendering, not in Effects or extra +state. + +```tsx +// 🔴 Bad: Unnecessary Effect for calculation +function Form() { + const [firstName, setFirstName] = useState("Taylor") + const [lastName, setLastName] = useState("Swift") + const [fullName, setFullName] = useState("") + + useEffect(() => { + setFullName(firstName + " " + lastName) + }, [firstName, lastName]) + // ... +} + +// ✅ Good: Calculate during rendering +function Form() { + const [firstName, setFirstName] = useState("Taylor") + const [lastName, setLastName] = useState("Swift") + const fullName = firstName + " " + lastName + // ... +} +``` + +• **Handling User Events:** Place logic for user actions (e.g., POST requests, +notifications) in event handlers, not Effects. + +```tsx +// 🔴 Bad: Event-specific logic inside an Effect +function ProductPage({ product, addToCart }) { + useEffect(() => { + if (product.isInCart) { + showNotification(`Added ${product.name} to the cart!`) + } + }, [product]) + + function handleBuyClick() { + addToCart(product) + } + // ... +} + +// ✅ Good: Event-specific logic in event handler +function ProductPage({ product, addToCart }) { + function handleBuyClick() { + addToCart(product) + showNotification(`Added ${product.name} to the cart!`) + } + // ... +} +``` + +• **Updating State Based on Props/State:** If a value can be derived from props +or state, compute it during render. Use `useMemo` only for expensive +calculations. + +```tsx +// 🔴 Bad: Redundant state and unnecessary Effect +function TodoList({ todos, filter }) { + const [visibleTodos, setVisibleTodos] = useState([]) + + useEffect(() => { + setVisibleTodos(getFilteredTodos(todos, filter)) + }, [todos, filter]) + // ... +} + +// ✅ Good: Calculate during rendering +function TodoList({ todos, filter }) { + const visibleTodos = getFilteredTodos(todos, filter) + // ... +} + +// ✅ Good: Use useMemo for expensive calculations +function TodoList({ todos, filter }) { + const visibleTodos = useMemo( + () => getFilteredTodos(todos, filter), + [todos, filter], + ) + // ... +} +``` + +• **Resetting State on Prop Change:** To reset all state when a prop changes, +use the `key` prop on the component. For partial resets, adjust state during +render or, preferably, lift state up. + +```tsx +// 🔴 Bad: Resetting state on prop change in an Effect +export default function ProfilePage({ userId }) { + const [comment, setComment] = useState("") + + useEffect(() => { + setComment("") + }, [userId]) + // ... +} + +// ✅ Good: Use key prop to reset state +export default function ProfilePage({ userId }) { + return +} + +function Profile({ userId }) { + const [comment, setComment] = useState("") + // State will reset automatically when key changes + // ... +} +``` + +• **Sharing Logic Between Event Handlers:** Extract shared logic into a function +called by each handler, not into an Effect. + +• **Notifying Parent Components:** Call parent callbacks directly in event +handlers, not in Effects. Or, lift state up to the parent. + +```tsx +// 🔴 Bad: Notifying parent in an Effect +function Toggle({ onChange }) { + const [isOn, setIsOn] = useState(false) + + useEffect(() => { + onChange(isOn) + }, [isOn, onChange]) + + function handleClick() { + setIsOn(!isOn) + } + // ... +} + +// ✅ Good: Notify parent in event handler +function Toggle({ onChange }) { + const [isOn, setIsOn] = useState(false) + + function handleClick() { + const nextIsOn = !isOn + setIsOn(nextIsOn) + onChange(nextIsOn) + } + // ... +} +``` + +• **Passing Data to Parent:** Fetch data in the parent and pass it down as +props, rather than having children update parent state via Effects. + +• **Chaining State Updates Solely to Trigger Effects:** Avoid chains of Effects +that update state to trigger other Effects. Instead, calculate during render and +update state in event handlers. + +```tsx +// 🔴 Bad: Chains of Effects +function Game() { + const [card, setCard] = useState(null) + const [goldCardCount, setGoldCardCount] = useState(0) + const [round, setRound] = useState(1) + + useEffect(() => { + if (card !== null && card.gold) { + setGoldCardCount((c) => c + 1) + } + }, [card]) + + useEffect(() => { + if (goldCardCount > 3) { + setRound((r) => r + 1) + setGoldCardCount(0) + } + }, [goldCardCount]) + // ... +} + +// ✅ Good: Calculate in event handler +function Game() { + const [card, setCard] = useState(null) + const [goldCardCount, setGoldCardCount] = useState(0) + const [round, setRound] = useState(1) + + function handlePlaceCard(nextCard) { + setCard(nextCard) + if (nextCard.gold) { + if (goldCardCount <= 3) { + setGoldCardCount(goldCardCount + 1) + } else { + setGoldCardCount(0) + setRound(round + 1) + } + } + } + // ... +} +``` + +• **App Initialization Logic:** For logic that should run once per app load (not +per mount), use a top-level flag or module-level code, not an Effect. + +```tsx +// 🔴 Bad: Effect that should only run once +function App() { + useEffect(() => { + loadDataFromLocalStorage() + checkAuthToken() + }, []) + // ... +} + +// ✅ Good: Use a flag or module-level code +let didInit = false + +function App() { + useEffect(() => { + if (!didInit) { + didInit = true + loadDataFromLocalStorage() + checkAuthToken() + } + }, []) + // ... +} + +// ✅ Better: Run at module level +if (typeof window !== "undefined") { + checkAuthToken() + loadDataFromLocalStorage() +} + +function App() { + // ... +} +``` + +##### When to Use `useEffect` + +• **Synchronizing with External Systems:** Use Effects to sync with non-React +systems (e.g., browser APIs, third-party widgets, subscriptions). + +```tsx +// ✅ Good: Subscribing to browser events +function useOnlineStatus() { + const [isOnline, setIsOnline] = useState(navigator.onLine) + + useEffect(() => { + function updateState() { + setIsOnline(navigator.onLine) + } + + window.addEventListener("online", updateState) + window.addEventListener("offline", updateState) + + return () => { + window.removeEventListener("online", updateState) + window.removeEventListener("offline", updateState) + } + }, []) + + return isOnline +} +``` + +• **Fetching Data:** Use Effects to fetch data that should stay in sync with +props/state. Always implement cleanup to avoid race conditions. + +```tsx +// ✅ Good: Data fetching with cleanup +function SearchResults({ query }) { + const [results, setResults] = useState([]) + + useEffect(() => { + let ignore = false + + fetchResults(query).then((json) => { + if (!ignore) { + setResults(json) + } + }) + + return () => { + ignore = true + } + }, [query]) + + return +} +``` + +• **Subscribing to External Stores:** Use Effects (or preferably +`useSyncExternalStore`) to subscribe to external data sources. + +```tsx +// ✅ Better: Using useSyncExternalStore +function useOnlineStatus() { + return useSyncExternalStore( + subscribe, + () => navigator.onLine, + () => true, + ) +} + +function subscribe(callback) { + window.addEventListener("online", callback) + window.addEventListener("offline", callback) + return () => { + window.removeEventListener("online", callback) + window.removeEventListener("offline", callback) + } +} +``` + +• **Logic Triggered by Component Display:** Use Effects for logic that should +run because the component was displayed (e.g., analytics events on mount). + +```tsx +// ✅ Good: Analytics on mount +function Form() { + useEffect(() => { + post("/analytics/event", { eventName: "visit_form" }) + }, []) + + // Form logic... +} +``` + +##### Best Practices & Alternatives + +• **Calculate Derived State During Render:** Don't store redundant state; +compute from existing state/props. + +• **Use `useMemo` for Expensive Calculations:** Only memoize if the calculation +is slow and depends on specific values. + +• **Use the `key` Prop to Reset State:** Changing the `key` will remount the +component and reset its state. + +• **Lift State Up:** If multiple components need to stay in sync, move state to +their common ancestor. + +• **Handle User Interactions in Event Handlers:** All logic tied to user actions +should be in event handlers, not Effects. + +• **Extract Reusable Logic:** Move shared logic into functions or custom hooks, +not Effects. + +• **App-Wide Initialization:** Use a module-level flag or code for logic that +must run once per app load. + +• **Use `useSyncExternalStore` for Subscriptions:** Prefer this over manual +Effect-based subscriptions for external data. + +• **Cleanup in Data Fetching Effects:** Always add cleanup to ignore stale +responses and prevent race conditions. + +• **Custom Hooks for Complex Effects:** Abstract complex Effect logic (like data +fetching) into custom hooks for clarity and reuse. + +• **Prefer Framework Data Fetching:** Use your framework's built-in data +fetching when possible for better performance and ergonomics. + +##### Summary + +Use `useEffect` only for synchronizing with external systems or when a side +effect is required because a component is displayed. For everything +else—especially data transformation, user events, and derived state—prefer +direct calculation, event handlers, or lifting state up. This leads to simpler, +faster, and more maintainable React code.