Skip to content

Commit 05de411

Browse files
committed
Fix infinite depth rendering issue with AiAutocomplete
1 parent 2ae7ddf commit 05de411

File tree

6 files changed

+99
-84
lines changed

6 files changed

+99
-84
lines changed

README.md

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -269,29 +269,6 @@ This project includes comprehensive [Cursor IDE](https://cursor.sh/) rules for e
269269
-**Responsive Design** - Works on desktop, tablet, and mobile
270270
-**Privacy-First** - All data stored locally in your browser
271271

272-
## 🗺️ Roadmap
273-
274-
### 🎯 Phase 1: Foundation (Current)
275-
- [x] Core policy analysis engine
276-
- [x] Basic UI and document upload
277-
- [x] Health profile management
278-
- [ ] Enhanced comparison algorithms
279-
- [ ] Export/sharing features
280-
281-
### 🚀 Phase 2: Advanced Features (Q1 2025)
282-
- [ ] Provider network analysis
283-
- [ ] Prescription drug coverage checker
284-
- [ ] HSA/FSA optimization calculator
285-
- [ ] Mobile app (React Native)
286-
- [ ] Multi-language support
287-
288-
### 🌟 Phase 3: Community Platform (Q2 2025)
289-
- [ ] User-generated policy reviews
290-
- [ ] Community-driven policy database
291-
- [ ] Integration with healthcare.gov
292-
- [ ] Enterprise/broker tools
293-
- [ ] API for third-party integrations
294-
295272
## 🔒 Privacy & Security
296273

297274
- **Local-first** - Your health data never leaves your device

app/health-profile/page.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client"
22

33
import * as React from "react"
4-
import { useState, useMemo, useEffect } from "react"
4+
import { useState, useMemo, useEffect, useCallback } from "react"
55
import { SidebarInset, SidebarTrigger } from "@/components/ui/sidebar"
66
import { Separator } from "@/components/ui/separator"
77
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from "@/components/ui/breadcrumb"
@@ -104,24 +104,29 @@ export default function HealthProfilePage() {
104104
}
105105

106106
// Handle member removal with announcement
107-
const handleRemoveMember = (memberId: string, index: number) => {
107+
const handleRemoveMember = useCallback((memberId: string, index: number) => {
108108
removeMember(memberId)
109-
announce(`Member ${index + 1} removed. Total members: ${members.length - 1}`)
110-
}
109+
announce(`Member ${index + 1} removed from health profile`)
110+
}, [removeMember, announce])
111111

112112
// Toggle card collapse state
113-
const toggleCardCollapse = (memberId: string) => {
113+
const toggleCardCollapse = useCallback((memberId: string) => {
114114
setCollapsedCards(prev => ({
115115
...prev,
116116
[memberId]: !prev[memberId]
117117
}))
118-
}
118+
}, [])
119119

120120
// Check if card is collapsed (default to open for first member, closed for others)
121121
const isCardCollapsed = (memberId: string, index: number) => {
122122
return collapsedCards[memberId] ?? (index > 0)
123123
}
124124

125+
// Memoize the updateMember callback to prevent infinite loops
126+
const handleUpdateMember = useCallback((memberId: string, updates: Partial<Member>) => {
127+
updateMember(memberId, updates)
128+
}, [updateMember])
129+
125130
return (
126131
<SidebarInset>
127132
<ScreenReaderAnnouncement announcementRef={announcementRef} />
@@ -160,9 +165,9 @@ export default function HealthProfilePage() {
160165
index={index}
161166
isCollapsed={isCardCollapsed(member.id, index)}
162167
canRemove={members.length > 1}
163-
onUpdate={(updates) => updateMember(member.id, updates)}
164-
onRemove={() => handleRemoveMember(member.id, index)}
165-
onToggleCollapse={() => toggleCardCollapse(member.id)}
168+
onUpdate={handleUpdateMember}
169+
onRemove={handleRemoveMember}
170+
onToggleCollapse={toggleCardCollapse}
166171
/>
167172
))}
168173
</div>

components/health-profile/medical-information.tsx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,46 @@
11
"use client"
22

3-
import { Label } from "@/components/ui/label"
43
import { AIAutocomplete } from "@/components/ui/ai-autocomplete"
4+
import { Label } from "@/components/ui/label"
55
import type { Member } from "@/lib/health-profile-store"
6+
import { useCallback, useEffect, useRef } from "react"
67

78
interface MedicalInformationProps {
89
member: Member
910
onUpdate: (updates: Partial<Member>) => void
1011
}
1112

1213
export function MedicalInformation({ member, onUpdate }: MedicalInformationProps) {
14+
// Use a ref to store the latest onUpdate function to avoid dependency issues
15+
const onUpdateRef = useRef(onUpdate)
16+
useEffect(() => {
17+
onUpdateRef.current = onUpdate
18+
}, [onUpdate])
19+
20+
// Create stable handlers that don't depend on any props
21+
const handleConditionsChange = useCallback((conditions: string[]) => {
22+
onUpdateRef.current({ conditions })
23+
}, [])
24+
25+
const handleMedicationsChange = useCallback((medications: string[]) => {
26+
onUpdateRef.current({ medications })
27+
}, [])
28+
29+
const handleAllergiesChange = useCallback((allergies: string[]) => {
30+
onUpdateRef.current({ allergies })
31+
}, [])
32+
1333
return (
1434
<div className="space-y-4">
1535
<div>
1636
<Label htmlFor={`conditions-${member.id}`} className="text-sm">Medical Conditions</Label>
1737
<AIAutocomplete
18-
id={`conditions-${member.id}`}
1938
placeholder="Type or select conditions (e.g., diabetes, hypertension)"
2039
value={member.conditions || []}
21-
onChange={(conditions) => onUpdate({ conditions })}
40+
onChange={handleConditionsChange}
2241
options={[]}
2342
searchType="conditions"
2443
className="mt-1"
25-
aria-label="Medical conditions"
26-
aria-describedby={`conditions-desc-${member.id}`}
2744
/>
2845
<span id={`conditions-desc-${member.id}`} className="sr-only">
2946
Enter or select medical conditions. You can add multiple conditions.
@@ -33,15 +50,12 @@ export function MedicalInformation({ member, onUpdate }: MedicalInformationProps
3350
<div>
3451
<Label htmlFor={`medications-${member.id}`} className="text-sm">Current Medications</Label>
3552
<AIAutocomplete
36-
id={`medications-${member.id}`}
3753
placeholder="Type or select medications"
3854
value={member.medications || []}
39-
onChange={(medications) => onUpdate({ medications })}
55+
onChange={handleMedicationsChange}
4056
options={[]}
4157
searchType="medications"
4258
className="mt-1"
43-
aria-label="Current medications"
44-
aria-describedby={`medications-desc-${member.id}`}
4559
/>
4660
<span id={`medications-desc-${member.id}`} className="sr-only">
4761
Enter or select current medications. You can add multiple medications.
@@ -51,15 +65,12 @@ export function MedicalInformation({ member, onUpdate }: MedicalInformationProps
5165
<div>
5266
<Label htmlFor={`allergies-${member.id}`} className="text-sm">Allergies</Label>
5367
<AIAutocomplete
54-
id={`allergies-${member.id}`}
5568
placeholder="Type or select allergies"
5669
value={member.allergies || []}
57-
onChange={(allergies) => onUpdate({ allergies })}
70+
onChange={handleAllergiesChange}
5871
options={[]}
5972
searchType="allergies"
6073
className="mt-1"
61-
aria-label="Known allergies"
62-
aria-describedby={`allergies-desc-${member.id}`}
6374
/>
6475
<span id={`allergies-desc-${member.id}`} className="sr-only">
6576
Enter or select known allergies. You can add multiple allergies.

components/health-profile/member-card.tsx

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
"use client"
22

3+
import { Badge } from "@/components/ui/badge"
34
import { Button } from "@/components/ui/button"
45
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
5-
import { Badge } from "@/components/ui/badge"
66
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
7-
import { ChevronDown, ChevronRight, User, X } from "lucide-react"
8-
import { cn } from "@/lib/utils"
97
import type { Member } from "@/lib/health-profile-store"
8+
import { cn } from "@/lib/utils"
9+
import { ChevronDown, ChevronRight, User, X } from "lucide-react"
10+
import { useCallback } from "react"
1011

1112
import { BasicInformation } from "./basic-information"
1213
import { LifestyleFactors } from "./lifestyle-factors"
13-
import { PregnancyInformation } from "./pregnancy-information"
1414
import { MedicalInformation } from "./medical-information"
1515
import { OtherServicesInput } from "./other-services-input"
16+
import { PregnancyInformation } from "./pregnancy-information"
1617

1718
interface MemberCardProps {
1819
member: Member
1920
index: number
2021
isCollapsed: boolean
2122
canRemove: boolean
22-
onUpdate: (updates: Partial<Member>) => void
23-
onRemove: () => void
24-
onToggleCollapse: () => void
23+
onUpdate: (memberId: string, updates: Partial<Member>) => void
24+
onRemove: (memberId: string, index: number) => void
25+
onToggleCollapse: (memberId: string) => void
2526
}
2627

2728
export function MemberCard({
@@ -33,9 +34,33 @@ export function MemberCard({
3334
onRemove,
3435
onToggleCollapse
3536
}: MemberCardProps) {
37+
// Create stable callbacks for child components
38+
const handleUpdate = useCallback((updates: Partial<Member>) => {
39+
onUpdate(member.id, updates)
40+
}, [member.id, onUpdate])
41+
42+
const handleRemove = useCallback(() => {
43+
onRemove(member.id, index)
44+
}, [member.id, index, onRemove])
45+
46+
const handleToggleCollapse = useCallback(() => {
47+
onToggleCollapse(member.id)
48+
}, [member.id, onToggleCollapse])
49+
50+
// Prevent onOpenChange from being called during render
51+
// Only handle user-initiated changes, not prop-driven changes
52+
const handleCollapsibleChange = useCallback((newOpen: boolean) => {
53+
// The Collapsible is controlled by !isCollapsed
54+
// So when newOpen is true, isCollapsed should be false
55+
// Only trigger the callback if this represents an actual user interaction
56+
if (newOpen !== !isCollapsed) {
57+
handleToggleCollapse()
58+
}
59+
}, [isCollapsed, handleToggleCollapse])
60+
3661
return (
3762
<Card className={cn("w-full transition-all", isCollapsed && "bg-gray-50 border-gray-200")}>
38-
<Collapsible open={!isCollapsed} onOpenChange={onToggleCollapse}>
63+
<Collapsible open={!isCollapsed} onOpenChange={handleCollapsibleChange}>
3964
<CollapsibleTrigger asChild>
4065
<CardHeader className="flex flex-row items-center justify-between p-3 cursor-pointer hover:bg-gray-50 transition-colors rounded-t-lg">
4166
<div className="flex items-center gap-2">
@@ -84,7 +109,7 @@ export function MemberCard({
84109
size="sm"
85110
onClick={(e) => {
86111
e.stopPropagation()
87-
onRemove()
112+
handleRemove()
88113
}}
89114
aria-label={`Remove ${index === 0 ? "Primary Member" : `Member ${index + 1}`}`}
90115
>
@@ -95,17 +120,17 @@ export function MemberCard({
95120
</CollapsibleTrigger>
96121
<CollapsibleContent>
97122
<CardContent className="space-y-4 pt-0">
98-
<BasicInformation member={member} onUpdate={onUpdate} />
123+
<BasicInformation member={member} onUpdate={handleUpdate} />
99124

100-
<LifestyleFactors member={member} onUpdate={onUpdate} />
125+
<LifestyleFactors member={member} onUpdate={handleUpdate} />
101126

102-
<PregnancyInformation member={member} onUpdate={onUpdate} />
127+
<PregnancyInformation member={member} onUpdate={handleUpdate} />
103128

104-
<MedicalInformation member={member} onUpdate={onUpdate} />
129+
<MedicalInformation member={member} onUpdate={handleUpdate} />
105130

106131
<OtherServicesInput
107132
services={member.otherServices || []}
108-
onServicesChange={(otherServices) => onUpdate({ otherServices })}
133+
onServicesChange={(otherServices) => handleUpdate({ otherServices })}
109134
/>
110135
</CardContent>
111136
</CollapsibleContent>

components/policy-overview.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const PolicyOverview: React.FC = () => {
2727
<CollapsibleTrigger className="w-full">
2828
<section className="w-full">
2929
<div className="text-sm flex flex-col md:flex-row gap-2 w-full bg-gray-50 rounded-lg p-3 border items-center justify-between">
30-
<div className="flex-1 min-w-[100px]">
30+
<div className="hidden md:block flex-1 min-w-[100px]">
3131
<div className="text-xs uppercase mb-1">Coverage Period</div>
3232
<div>
3333
{coverage_period.end_date === '<UNKNOWN>'
@@ -36,15 +36,15 @@ export const PolicyOverview: React.FC = () => {
3636
}
3737
</div>
3838
</div>
39-
<div className="flex-1 min-w-[100px]">
39+
<div className="hidden md:block flex-1 min-w-[100px]">
4040
<div className="text-xs uppercase mb-1">Coverage For</div>
4141
<div>{coverage_for.replaceAll('_', ' ')}</div>
4242
</div>
4343
<div className="flex-1 min-w-[100px]">
4444
<div className="text-xs uppercase mb-1">Issuer</div>
4545
<div>{issuer_name} ({plan_type})</div>
4646
</div>
47-
<div className="flex items-center">
47+
<div className="hidden md:flex items-center">
4848
{isOpen ? (
4949
<ChevronUp className="h-4 w-4" />
5050
) : (

0 commit comments

Comments
 (0)