Describe the bug
VizarrViewerComponent creates a new jotai atom (viewStateAtomWithEffect) directly inside the component function body on every render. This atom is passed as the value of ViewStateContext.Provider, so its reference changes on every render, causing all context consumers (Viewer, LayerFitToViewportButton, etc.) to re-render. In Viewer.tsx, setViewState changing reference causes resetViewState (via useCallback) to be recreated, which triggers a useEffect that calls setViewState, perpetuating the loop. React terminates this with:
Error: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
To Reproduce
- Render
<VizarrViewer sources={["..."]} onViewStateChange={fn} />
- Open the browser console
- Observe "Maximum update depth exceeded" immediately on load
Expected behavior
The viewer loads and renders the image without triggering an infinite re-render loop.
Root cause
// BUG: creates a new atom instance on every render
const viewStateAtomWithEffect: PrimitiveAtom<ViewState | null> = atom(
(get) => get(viewStateAtom),
(get, set, update) => { ... onViewStateChange?.(...) ... }
);
A jotai atom created inside a component body is a new object reference on every render. Passing it to Context.Provider value={...} invalidates the context for all consumers on every render.
Fix
Stabilise the atom with React.useState's factory form (runs once on mount), and keep the onViewStateChange callback current via React.useRef:
const onViewStateChangeRef = React.useRef(onViewStateChange);
React.useLayoutEffect(() => {
onViewStateChangeRef.current = onViewStateChange;
}, [onViewStateChange]);
const [viewStateAtomWithEffect] = React.useState<PrimitiveAtom<ViewState | null>>(() =>
atom(
(get) => get(viewStateAtom),
(get, set, update) => {
const viewState = typeof update === "function" ? update(get(viewStateAtom)) : update;
if (viewState) {
onViewStateChangeRef.current?.({ target: viewState.target, zoom: viewState.zoom });
set(viewStateAtom, update);
}
},
)
);
Additional context
Issue #89 was originally reported against this bug. PR #95 references it as fixed but additional work appears to be needed. This issue can track.
Describe the bug
VizarrViewerComponentcreates a new jotai atom (viewStateAtomWithEffect) directly inside the component function body on every render. This atom is passed as the value ofViewStateContext.Provider, so its reference changes on every render, causing all context consumers (Viewer,LayerFitToViewportButton, etc.) to re-render. InViewer.tsx,setViewStatechanging reference causesresetViewState(viauseCallback) to be recreated, which triggers auseEffectthat callssetViewState, perpetuating the loop. React terminates this with:To Reproduce
<VizarrViewer sources={["..."]} onViewStateChange={fn} />Expected behavior
The viewer loads and renders the image without triggering an infinite re-render loop.
Root cause
A jotai atom created inside a component body is a new object reference on every render. Passing it to
Context.Provider value={...}invalidates the context for all consumers on every render.Fix
Stabilise the atom with
React.useState's factory form (runs once on mount), and keep theonViewStateChangecallback current viaReact.useRef:Additional context
Issue #89 was originally reported against this bug. PR #95 references it as fixed but additional work appears to be needed. This issue can track.