Last active
January 18, 2026 18:59
-
-
Save matthew-gerstman/1ac6a1a64e5c899f9a9a3b20f7eaedf8 to your computer and use it in GitHub Desktop.
Mirror Hooks Optimization Proposal
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Mirror Hooks Optimization Proposal | |
| ## Overview | |
| This document proposes a comprehensive approach to optimizing Mirror hooks to reduce unnecessary re-renders. It builds on two complementary changes: | |
| 1. **PR #3830** - Refactor `useMirrorList` to use derived atoms with shallow equality | |
| 2. **PR #3754** - Add selector hooks (`useMirrorListSelector`, `useMirrorFilteredMap`) for advanced cases | |
| ## Current State | |
| ### Problem: Unnecessary Re-renders | |
| The original `useMirrorList` subscribes to the entire collection: | |
| ```typescript | |
| // Old implementation | |
| const allResources = useAtomValue(getResourceListAtom(mirror.getKey(), resourceType)) | |
| const filteredResults = useMemo(() => { | |
| return allResources.filter(filterFn) | |
| }, [allResources, filterFn]) | |
| ``` | |
| **Issue:** When ANY item in the collection changes, the component re-renders—even if the filtered result is identical. | |
| ## Solution Architecture | |
| ### Layer 1: Optimized `useMirrorList` (PR #3830) | |
| For the common case of simple filtering that returns `T[]`: | |
| ```typescript | |
| // New implementation uses selectAtom with shallow equality | |
| const filteredAtom = useMemo(() => { | |
| return selectAtom( | |
| baseAtom, | |
| (items) => filterFn ? items.filter(filterFn) : items, | |
| shallowArrayEqual // Only re-render if filtered result actually changed | |
| ) | |
| }, [mirror, resourceType, depsKey]) | |
| return useAtomValue(filteredAtom) | |
| ``` | |
| **Benefit:** Components only re-render when their filtered results actually change. | |
| ### Layer 2: Selector Hooks (PR #3754) | |
| For advanced cases requiring transformations or derived values: | |
| | Hook | Use Case | Return Type | | |
| |------|----------|-------------| | |
| | `useMirrorListSelector<T, R>` | Derive any value from list | `R` | | |
| | `useMirrorResourceSelector<T, R>` | Derive value from single resource | `R` | | |
| | `useMirrorFilteredMap<T, R>` | Filter + transform | `R[]` | | |
| ## Coverage Analysis | |
| | Scenario | Solution | Status | | |
| |----------|----------|--------| | |
| | Simple filtering → `T[]` | `useMirrorList` | ✅ PR #3830 | | |
| | Derived values → `R` | `useMirrorListSelector` | ✅ PR #3754 | | |
| | Filter + Map → `R[]` | `useMirrorFilteredMap` | ✅ PR #3754 | | |
| | Single resource field | `useMirrorResourceSelector` | ✅ PR #3754 | | |
| | Global list filtering | `useGlobalMirrorList` | ❌ Not optimized | | |
| --- | |
| ## Missing Optimizations with Real-World Examples | |
| ### 1. `useGlobalMirrorList` (High Priority) - 62 usages | |
| Same problem as old `useMirrorList`—needs derived atom fix. | |
| **Real examples from codebase:** | |
| ```typescript | |
| // use-workspaces.ts - re-renders on ANY workspace change | |
| const workspaces = useGlobalMirrorList<Workspace>('workspaces') | |
| // use-teams.ts - re-renders on ANY team change in workspace | |
| const allTeams = useGlobalMirrorList<Team>('teams', | |
| (team) => team.workspaceId === workspaceId, [workspaceId]) | |
| // use-project-members.ts - re-renders on ANY grant change | |
| const projectGrants = useGlobalMirrorList<any>('resource-access-grants', | |
| projectGrantsFilter, [projectId]) | |
| const projectInvitations = useGlobalMirrorList<any>('resource-invitations', | |
| projectInvitationsFilter, [projectId]) | |
| const relevantUsers = useGlobalMirrorList<any>('users') | |
| const allWorkspaces = useGlobalMirrorList<any>('workspaces') | |
| const allTeams = useGlobalMirrorList<any>('teams') | |
| // use-team-project-access.ts - re-renders on ANY grant change | |
| const grants = useGlobalMirrorList<ResourceAccessGrant>('resource-access-grants') | |
| // use-projects.ts - re-renders on ANY project change | |
| const allProjects = useGlobalMirrorList<Project>('projects') | |
| const resourceGrants = useGlobalMirrorList<ResourceAccessGrant>('resource-access-grants') | |
| ``` | |
| **Fix:** Apply same `selectAtom` + `shallowArrayEqual` pattern as `useMirrorList`. | |
| --- | |
| ### 2. Count Patterns (Medium Priority) | |
| Common pattern: get list just to count items. | |
| **Real examples from codebase:** | |
| ```typescript | |
| // use-thread-todos.ts - filters 3x just for counts | |
| export function useThreadTodoStats(threadId: string) { | |
| const todos = useThreadTodos(threadId) | |
| return useMemo(() => { | |
| const total = todos.length | |
| const completed = todos.filter((todo) => todo.status === 'completed').length | |
| const inProgress = todos.filter((todo) => todo.status === 'in_progress').length | |
| const pending = todos.filter((todo) => todo.status === 'pending').length | |
| return { total, completed, inProgress, pending } | |
| }, [todos]) | |
| } | |
| // use-notification-bell.ts - filters just for count | |
| const unreadCount = notifications.filter((n) => !n.readAt).length | |
| ``` | |
| **Proposed convenience hook:** | |
| ```typescript | |
| // Instead of filtering then counting | |
| const count = useMirrorCount<Todo>('thread_todos', | |
| (t) => t.threadId === threadId && t.status === 'completed', [threadId]) | |
| ``` | |
| --- | |
| ### 3. Find Single Item Patterns (Medium Priority) | |
| Common pattern: get entire list just to find one item. | |
| **Real examples from codebase:** | |
| ```typescript | |
| // project-page.tsx - finds active thread from all threads | |
| const activeThread = useMemo(() => | |
| allThreads.find((t) => t.id === activeThreadId), [allThreads, activeThreadId]) | |
| // project-page.tsx - finds active artifact | |
| const activeArtifact = useMemo( | |
| () => (artifactId ? allProjectArtifacts.find((a) => a.id === artifactId) : undefined), | |
| [artifactId, allProjectArtifacts] | |
| ) | |
| // skill-detail-page.tsx - finds specific file | |
| const skillMd = files.find((f) => f.filename === 'SKILL.md' && !f.folder) | |
| ``` | |
| **Proposed convenience hook:** | |
| ```typescript | |
| // Instead of list + find | |
| const activeThread = useMirrorFind<Thread>('threads', | |
| (t) => t.id === activeThreadId, [activeThreadId]) | |
| ``` | |
| --- | |
| ### 4. Sorted Results (Medium Priority) | |
| Sorting creates new array references, breaking shallow equality. | |
| **Real examples from codebase:** | |
| ```typescript | |
| // skill-detail-page.tsx - sort after filter | |
| const allSkillFiles = useMirrorList<SkillFile>('skill-files') | |
| const files = useMemo(() => { | |
| return allSkillFiles | |
| .filter((file) => file.sessionId === skillId) | |
| .sort((a, b) => a.order - b.order) | |
| }, [allSkillFiles, skillId]) | |
| // project-page.tsx - sort threads | |
| const projectThreads = useMemo( | |
| () => allThreads.filter((thread) => thread.projectId === projectId), | |
| [allThreads, projectId] | |
| ) | |
| // Later sorts or finds on projectThreads | |
| ``` | |
| **Proposed convenience hook:** | |
| ```typescript | |
| // Stable sorted list with ID-based equality | |
| const files = useMirrorSortedList<SkillFile>('skill-files', | |
| (file) => file.sessionId === skillId, | |
| (a, b) => a.order - b.order, | |
| [skillId] | |
| ) | |
| ``` | |
| --- | |
| ### 5. Existence Check Patterns (Low Priority) | |
| Common pattern: check if any item matches. | |
| **Real examples from codebase:** | |
| ```typescript | |
| // projects-library-page.tsx - checks if project is favorited | |
| starred: favorites.some((fav) => fav.resourceId === apiProject.id && !fav.deletedAt) | |
| // use-team-project-access.ts - checks if project is shared | |
| return isProjectSharedWithTeam(grants, projectId, teamId) | |
| // which internally uses: grants.some(...) | |
| ``` | |
| **Proposed convenience hook:** | |
| ```typescript | |
| const isFavorited = useMirrorExists<Favorite>('favorites', | |
| (f) => f.resourceId === projectId && !f.deletedAt, [projectId]) | |
| ``` | |
| --- | |
| ### 6. Dependent/Chained Queries (Low Priority) | |
| Currently awkward to express queries that depend on other queries. | |
| **Real example from new-artifact-route.tsx:** | |
| ```typescript | |
| // Step 1: Get workbook IDs for project | |
| const projectWorkbookIds = useMirrorListSelector<Artifact, Set<string>>( | |
| 'artifacts', | |
| (a) => new Set(a.filter(x => x.projectId === pid && x.type === 'workbook').map(x => x.resourceId)), | |
| [pid], | |
| setEqual | |
| ) | |
| // Step 2: Filter sheets by those IDs (awkward deps serialization) | |
| const projectSheets = useMirrorListSelector<Sheet, Sheet[]>( | |
| 'sheets', | |
| (s) => s.filter(x => projectWorkbookIds.has(x.workbookId)), | |
| [[...projectWorkbookIds].sort()] // Have to serialize Set for deps | |
| ) | |
| ``` | |
| --- | |
| ## Implementation Roadmap | |
| ### Phase 1: Core Optimizations (Ready) | |
| - [x] PR #3830: `useMirrorList` derived atoms | |
| - [x] PR #3754: Selector hooks | |
| ### Phase 2: Complete Coverage | |
| - [ ] Apply derived atom fix to `useGlobalMirrorList` (62 usages) | |
| - [ ] Add `useMirrorCount` convenience hook | |
| - [ ] Add `useMirrorFind` convenience hook | |
| - [ ] Add `useMirrorExists` convenience hook | |
| ### Phase 3: Advanced Patterns (Future) | |
| - [ ] `useMirrorSortedList` with stable equality | |
| - [ ] Query builder for dependent/chained queries | |
| --- | |
| ## Migration Guide | |
| ### When to use each hook | |
| ``` | |
| Need filtered list of same type? | |
| → useMirrorList('type', filterFn, deps) | |
| Need derived value (count, IDs, single item)? | |
| → useMirrorListSelector('type', selector, deps) | |
| Need filter + transform to different shape? | |
| → useMirrorFilteredMap('type', filterFn, mapFn, deps) | |
| Need specific field from single resource? | |
| → useMirrorResourceSelector('type', id, selector) | |
| Need to check if any item matches? (future) | |
| → useMirrorExists('type', filterFn, deps) | |
| Need count of matching items? (future) | |
| → useMirrorCount('type', filterFn, deps) | |
| Need to find single matching item? (future) | |
| → useMirrorFind('type', filterFn, deps) | |
| ``` | |
| ### Example Refactors | |
| **Before (re-renders on any artifact change):** | |
| ```typescript | |
| const artifacts = useMirrorList<Artifact>('artifacts') | |
| const suggestions = useMemo(() => | |
| artifacts.filter(a => a.type === 'suggestion'), [artifacts]) | |
| ``` | |
| **After (re-renders only when suggestions change):** | |
| ```typescript | |
| const suggestions = useMirrorList<Artifact>('artifacts', | |
| a => a.type === 'suggestion', []) | |
| ``` | |
| --- | |
| **Before (re-renders on any team-member change):** | |
| ```typescript | |
| const members = useMirrorList<TeamMember>('team-members') | |
| const myTeamIds = useMemo(() => | |
| members.filter(m => m.userId === id).map(m => m.teamId), [members, id]) | |
| ``` | |
| **After (re-renders only when user's teams change):** | |
| ```typescript | |
| const myTeamIds = useMirrorListSelector<TeamMember, string[]>('team-members', | |
| m => m.filter(x => x.userId === id).map(x => x.teamId), [id]) | |
| ``` | |
| --- | |
| **Before (re-renders on any todo change):** | |
| ```typescript | |
| const todos = useThreadTodos(threadId) | |
| const completedCount = todos.filter(t => t.status === 'completed').length | |
| ``` | |
| **After (future - re-renders only when count changes):** | |
| ```typescript | |
| const completedCount = useMirrorCount<Todo>('thread_todos', | |
| t => t.threadId === threadId && t.status === 'completed', [threadId]) | |
| ``` | |
| --- | |
| ## Summary | |
| The combination of optimized `useMirrorList` (PR #3830) and selector hooks (PR #3754) provides a comprehensive solution for most re-render optimization needs. | |
| **Immediate priority:** Apply derived atom fix to `useGlobalMirrorList` (62 usages across the codebase). | |
| **Future priorities:** Add convenience hooks for common patterns (count, exists, find) to reduce boilerplate and improve DX. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment