From 68e6acd50463b212e1f06e7ecbbccbe2796dea5e Mon Sep 17 00:00:00 2001 From: Tim Ren <137012659+xr843@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:07:12 +0800 Subject: [PATCH] fix(web): memoize Zero provider opts to prevent reconnect churn Wrap the opts object and derived context in useMemo so ZeroReactProvider receives stable references across parent re-renders. Before this change opts was rebuilt on every render of ZeroProvider, which can cause the Rocicorp Zero client to churn its internal state / reconnect if it compares props by reference. Fixes #1097. --- .../components/providers/ZeroProvider.tsx | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/surfsense_web/components/providers/ZeroProvider.tsx b/surfsense_web/components/providers/ZeroProvider.tsx index c97eac072..1ee7a0501 100644 --- a/surfsense_web/components/providers/ZeroProvider.tsx +++ b/surfsense_web/components/providers/ZeroProvider.tsx @@ -6,7 +6,7 @@ import { ZeroProvider as ZeroReactProvider, } from "@rocicorp/zero/react"; import { useAtomValue } from "jotai"; -import { useEffect, useRef } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { getBearerToken, handleUnauthorized, refreshAccessToken } from "@/lib/auth-utils"; import { queries } from "@/zero/queries"; @@ -43,19 +43,30 @@ function ZeroAuthSync() { export function ZeroProvider({ children }: { children: React.ReactNode }) { const { data: user } = useAtomValue(currentUserAtom); - const hasUser = !!user?.id; - const userID = hasUser ? String(user.id) : "anon"; - const context = hasUser ? { userId: String(user.id) } : undefined; + const userId = user?.id; + const hasUser = !!userId; + const userID = hasUser ? String(userId) : "anon"; + // getBearerToken() returns a string (a primitive), so it's safe to read + // on every render — reference equality holds as long as the token is + // unchanged, which keeps the memoized `opts` below stable. const auth = hasUser ? getBearerToken() || undefined : undefined; - const opts = { - userID, - schema, - queries, - context, - cacheURL, - auth, - }; + const context = useMemo( + () => (hasUser ? { userId: String(userId) } : undefined), + [hasUser, userId], + ); + + const opts = useMemo( + () => ({ + userID, + schema, + queries, + context, + cacheURL, + auth, + }), + [userID, context, auth], + ); return (