SEARCH INFINITE LOOP FIX
Comprehensive documentation system guides
Search: Infinite Loop Fix (untracked signals)
Date: October 29, 2025
Time: Evening Session (Critical Fix)
Status: ✅ COMPLETE
Purpose: Fix infinite loop in search caused by reactive signal dependencies
🐛 Critical Problem: Infinite Loop
User Report
"The global search is broken. Seems to have infinite loop issues."
Evidence
- Browser console showed 1,030+ messages (screenshot)
- Search modal stuck showing "Searching..."
- Console flooded with repeated log messages
- Browser becoming unresponsive
🔍 Root Cause Analysis
The Problematic Code
// 🎯 PROJECT-SCOPED: React to project changes from state
effect(() => {
const project = this.currentProject();
console.log(`🔍 Project changed to: ${project}`);
// ❌ PROBLEM: This reads searchTerm() which makes the effect depend on it!
const currentSearchTerm = this.searchTerm();
if (currentSearchTerm && currentSearchTerm.trim()) {
console.log(`🔄 Re-running search "${currentSearchTerm}" in new project: ${project}`);
setTimeout(() => {
this.search(currentSearchTerm); // ← This updates searchTerm!
}, 100);
} else {
this.searchResults.set([]);
}
// ❌ ALSO PROBLEMATIC: Reading projectOrder() makes effect depend on it
const currentOrder = this.projectOrder();
if (project && project !== 'public' && !currentOrder.includes(project)) {
this.projectOrder.set([project, ...currentOrder]); // ← This updates projectOrder!
}
});
The Infinite Loop Cycle
1. Effect runs (triggered by project change)
↓
2. Reads searchTerm() → Effect now depends on searchTerm
↓
3. Calls this.search(term)
↓
4. search() updates searchTerm signal
↓
5. searchTerm change triggers effect AGAIN!
↓
6. Back to step 1 → INFINITE LOOP! 🔄
Why This Happened
Angular signals use reactive tracking:
- When an
effect()reads a signal, it automatically becomes dependent on that signal - When that signal changes, the effect re-runs
- If the effect changes the signal it reads → infinite loop!
The Problem:
- We wanted to react ONLY to
currentProject()changes - But by reading
searchTerm()inside the effect, we made it reactive to BOTH signals - This created an unintended dependency chain
✅ The Solution: untracked()
What is untracked()?
untracked() is an Angular utility that lets you read a signal without creating a reactive dependency.
// ✅ CREATES dependency (effect will re-run when signal changes)
const value = this.mySignal();
// ✅ NO dependency (signal can change without triggering effect)
const value = untracked(() => this.mySignal());
The Fixed Code
import { Injectable, signal, computed, inject, effect, untracked } from '@angular/core';
// 🎯 PROJECT-SCOPED: React to project changes from state
effect(() => {
const project = this.currentProject(); // ← Still reactive to this
// ✅ FIX: Use untracked() to read searchTerm without making effect depend on it
const currentSearchTerm = untracked(() => this.searchTerm());
if (currentSearchTerm && currentSearchTerm.trim()) {
setTimeout(() => {
this.search(currentSearchTerm);
}, 100);
} else {
this.searchResults.set([]);
}
// ✅ FIX: Also use untracked for projectOrder
const currentOrder = untracked(() => this.projectOrder());
if (project && project !== 'public' && !currentOrder.includes(project)) {
this.projectOrder.set([project, ...currentOrder]);
}
});
How It Works Now
1. Effect runs (triggered by project change ONLY)
↓
2. Reads searchTerm() via untracked() → NO dependency created
↓
3. Calls this.search(term)
↓
4. search() updates searchTerm signal
↓
5. searchTerm change does NOT trigger effect ✅
↓
6. Loop broken! 🎉
🧹 Bonus: Console Spam Cleanup
While fixing the infinite loop, also removed excessive console logging that was making the issue worse:
Removed from performSearch()
// ❌ Before
console.log('🔍 performSearch called with term:', term);
console.log('🎯 Search mode:', mode);
Removed from performProjectFilter()
// ❌ Before
console.log('🗂️ Performing project filter with term:', term);
Removed from performContentSearch()
// ❌ Before (18 lines!)
console.log('========================================');
console.log('🔍 CONTENT SEARCH STARTING');
console.log('🔍 Search term:', term);
console.log('🔍 Search index length:', this.searchIndex.length);
console.log('🔍 Search index items:', this.searchIndex.map(...));
console.log('🔍 Search terms (split):', searchTerms);
console.log('🔍 Active language from service:', activeLanguage);
console.log('🔍 Current project:', currentProject);
console.log('🎯 Global search enabled:', isGlobalSearchEnabled);
console.log('🔍 Effective search config:', effectiveConfig);
console.log(`🔒 Blocked ${i.title} - language mismatch...`); // ← Inside filter loop!
console.log('🔍 Selected projects:', selectedProjects);
console.log('🔍 Filtered index after access control:', filteredIndex.length);
console.log('========================================');
console.log('🔍 STARTING RELEVANCE SCORING');
console.log('🔍 SEARCH COMPLETE');
console.log('🔍 Total results found:', sortedResults.length);
console.log('🔍 Results:', sortedResults.map(...));
console.log('🔍 Paginated results:', paginatedResults.length);
Removed from effect
// ❌ Before
console.log(`🔍 Project changed to: ${project}`);
console.log(`🔄 Re-running search "${currentSearchTerm}" in new project: ${project}`);
Result: Clean console, better performance, easier to debug real issues!
📊 Impact
Before (Broken)
| Metric | Value | Issue |
|---|---|---|
| Console Messages | 1,030+ | Infinite loop spam |
| Search Status | "Searching..." forever | Never completes |
| Browser | Unresponsive | Too many logs |
| Performance | Terrible | CPU maxed out |
| User Experience | Broken | Can't search |
After (Fixed)
| Metric | Value | Improvement |
|---|---|---|
| Console Messages | ~0 | ✅ Clean |
| Search Status | Shows results | ✅ Works perfectly |
| Browser | Responsive | ✅ Smooth |
| Performance | Excellent | ✅ No lag |
| User Experience | Perfect | ✅ Search works! |
🎓 Key Lessons
1. Understand Signal Dependencies
// Creates dependency
effect(() => {
const value = this.signal(); // ← Effect will re-run when signal changes
});
// Prevents dependency
effect(() => {
const value = untracked(() => this.signal()); // ← Signal changes ignored
});
2. Be Careful with Effects That Write
// ⚠️ DANGER ZONE
effect(() => {
const value = this.signal(); // Read signal
this.signal.set(newValue); // Write to same signal → LOOP!
});
// ✅ SAFE
effect(() => {
const trigger = this.triggerSignal(); // Read trigger
const value = untracked(() => this.signal()); // Read data (no dependency)
this.signal.set(newValue); // Write is safe now
});
3. Console Logging Can Hide Issues
- Excessive logging slowed down the browser
- Made it harder to see the real problem
- Removed 20+ log statements
- Now only log important events
📁 Files Modified
apps/docs-angular/src/app/services/search.service.ts- Imports: Added
untrackedto imports from@angular/core - Effect: Wrapped
searchTerm()andprojectOrder()reads inuntracked() - Logging: Removed 20+ console.log statements from search methods
- Lines Changed: ~30 lines
- Imports: Added
🧪 Testing
Test 1: Search Works
1. Open search (Cmd/Ctrl + K)
2. Type "ji"
3. ✅ Results appear immediately
4. ✅ Console clean (no spam)
5. ✅ Browser responsive
Test 2: Project Switch
1. Search for "test" in Project A
2. Switch to Project B
3. ✅ Search re-runs automatically
4. ✅ Shows results from Project B
5. ✅ No infinite loop
6. ✅ Console clean
Test 3: Console Check
1. Open browser console
2. Perform several searches
3. Switch between projects
4. ✅ No repeated messages
5. ✅ No infinite loops
6. ✅ Console stays clean
✅ Verification Checklist
- Search works correctly
- No infinite loops
- Console is clean
- Project switching works
- Auto re-search works
- Performance is excellent
- No linter errors
-
untrackedimported correctly - All console spam removed
🎉 Status
COMPLETE - The infinite loop issue is completely fixed using Angular's untracked() utility. Search now works perfectly with clean console output and excellent performance.
Key Fixes
✅ Infinite Loop Fixed: Used untracked() to break dependency cycle
✅ Console Cleaned: Removed 20+ unnecessary log statements
✅ Performance Restored: Search is fast and responsive
✅ User Experience: Search works perfectly again
Technical Achievement
This fix demonstrates proper use of Angular signals' reactive system:
- Understanding when to create dependencies (reactive)
- Understanding when to avoid dependencies (untracked)
- Proper separation of trigger signals vs. data signals
- Clean, maintainable code
The search is now production-ready! 🚀