Skip to content

Commit 9de91fd

Browse files
Lazy build refs to prevent rebuilding of potentially large maps every render
1 parent 8b9fa4f commit 9de91fd

1 file changed

Lines changed: 16 additions & 8 deletions

File tree

src/components/AnalysisStore.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,22 @@ export function AnalysisStoreProvider({
109109
const analysisRef = useRef<TextAnalysis>(initialAnalysis ?? EMPTY_ANALYSIS);
110110
const listenersRef = useRef(new Set<() => void>());
111111

112+
// These two indexes are built lazily via ??= so that passing an initializer expression to useRef
113+
// (which evaluates on every render but is only used on the first mount) doesn't rebuild large Maps
114+
// across a full-Bible analysis on every re-render.
115+
112116
/** Pre-built map of `TokenAnalysis.id` → `TokenAnalysis` for O(1) lookup by id. */
113-
const analysisByIdRef = useRef<Map<string, TokenAnalysis>>(
114-
new Map(analysisRef.current.tokenAnalyses.map((ta) => [ta.id, ta])),
115-
);
117+
const analysisByIdRef = useRef<Map<string, TokenAnalysis> | undefined>(undefined);
118+
analysisByIdRef.current ??= new Map(analysisRef.current.tokenAnalyses.map((ta) => [ta.id, ta]));
116119

117120
/**
118121
* Pre-built map of `tokenRef` → approved `TokenAnalysis.id` for the active language. Reset on
119122
* every mutation that changes the analysis.
120123
*/
121-
const approvedAnalysisIdByTokenRef = useRef<Map<string, string>>(
122-
buildApprovedGlossIndex(analysisRef.current, analysisByIdRef.current),
124+
const approvedAnalysisIdByTokenRef = useRef<Map<string, string> | undefined>(undefined);
125+
approvedAnalysisIdByTokenRef.current ??= buildApprovedGlossIndex(
126+
analysisRef.current,
127+
analysisByIdRef.current,
123128
);
124129

125130
/**
@@ -145,9 +150,11 @@ export function AnalysisStoreProvider({
145150
*/
146151
const getGloss = useCallback(
147152
(tokenRef: string) => {
148-
const analysisId = approvedAnalysisIdByTokenRef.current.get(tokenRef);
153+
// eslint-disable-next-line no-type-assertion/no-type-assertion -- ??= above guarantees non-null; TS can't see through the closure boundary
154+
const analysisId = approvedAnalysisIdByTokenRef.current!.get(tokenRef);
149155
if (!analysisId) return '';
150-
const ta = analysisByIdRef.current.get(analysisId);
156+
// eslint-disable-next-line no-type-assertion/no-type-assertion -- same: ??= guarantees non-null
157+
const ta = analysisByIdRef.current!.get(analysisId);
151158
/* v8 ignore next -- optional chaining on ta?.gloss produces a branch V8 cannot reach through the mock */
152159
return ta?.gloss?.[analysisLanguage] ?? '';
153160
},
@@ -196,7 +203,8 @@ export function AnalysisStoreProvider({
196203
};
197204

198205
analysisRef.current = next;
199-
analysisByIdRef.current = new Map([...analysisByIdRef.current, [id, newAnalysis]]);
206+
// eslint-disable-next-line no-type-assertion/no-type-assertion -- ??= above guarantees non-null; TS can't see through the closure boundary
207+
analysisByIdRef.current = new Map([...analysisByIdRef.current!, [id, newAnalysis]]);
200208
approvedAnalysisIdByTokenRef.current = buildApprovedGlossIndex(next, analysisByIdRef.current);
201209

202210
listenersRef.current.forEach((l) => l());

0 commit comments

Comments
 (0)