@@ -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