From 865d2d70cce2cf096827ee7a590ddd4164445e4e Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 13 Mar 2026 17:05:44 -0700 Subject: [PATCH] Avoid spurious re-renders in context providers More aggressive memoization of theme provider and related components to avoid over re-rendering of components that depend on things like the resolvedColorMode. Additionally add useMemo: * error boundary - prevent high level app-wrap re-renders * EventLoopProvider - prevents event loop re-rendering when the location/params/navigate hooks change --- .../radix_themes_color_mode_provider.js | 55 +++++++++++-------- reflex/.templates/web/utils/react-theme.js | 2 +- reflex/.templates/web/utils/state.js | 2 +- reflex/app.py | 13 +++-- reflex/compiler/templates.py | 4 +- 5 files changed, 44 insertions(+), 32 deletions(-) diff --git a/reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js b/reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js index 46c4471a2e1..fdd5bcaebaf 100644 --- a/reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +++ b/reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js @@ -1,24 +1,27 @@ import { useTheme } from "$/utils/react-theme"; -import { createElement, useEffect } from "react"; +import { createElement, useCallback, useEffect, useMemo } from "react"; import { ColorModeContext, defaultColorMode } from "$/utils/context"; export default function RadixThemesColorModeProvider({ children }) { const { theme, resolvedTheme, setTheme } = useTheme(); - const toggleColorMode = () => { + const toggleColorMode = useCallback(() => { setTheme(resolvedTheme === "light" ? "dark" : "light"); - }; + }, [resolvedTheme, setTheme]); - const setColorMode = (mode) => { - const allowedModes = ["light", "dark", "system"]; - if (!allowedModes.includes(mode)) { - console.error( - `Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`, - ); - mode = defaultColorMode; - } - setTheme(mode); - }; + const setColorMode = useCallback( + (mode) => { + const allowedModes = ["light", "dark", "system"]; + if (!allowedModes.includes(mode)) { + console.error( + `Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`, + ); + mode = defaultColorMode; + } + setTheme(mode); + }, + [setTheme], + ); useEffect(() => { const radixRoot = document.querySelector( @@ -30,16 +33,20 @@ export default function RadixThemesColorModeProvider({ children }) { } }, [resolvedTheme]); - return createElement( - ColorModeContext.Provider, - { - value: { - rawColorMode: theme, - resolvedColorMode: resolvedTheme, - toggleColorMode, - setColorMode, - }, - }, - children, + return useMemo( + () => + createElement( + ColorModeContext.Provider, + { + value: { + rawColorMode: theme, + resolvedColorMode: resolvedTheme, + toggleColorMode, + setColorMode, + }, + }, + children, + ), + [theme, resolvedTheme, toggleColorMode, setColorMode, children], ); } diff --git a/reflex/.templates/web/utils/react-theme.js b/reflex/.templates/web/utils/react-theme.js index e11f3ef2eff..ab0290a5a23 100644 --- a/reflex/.templates/web/utils/react-theme.js +++ b/reflex/.templates/web/utils/react-theme.js @@ -69,7 +69,7 @@ export function ThemeProvider({ children, defaultTheme = "system" }) { return () => { mediaQuery.removeEventListener("change", handleChange); }; - }); + }, []); // Save theme to localStorage whenever it changes // Skip saving only if theme key already exists and we haven't initialized yet diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 9e937ed62cd..292c129cfc6 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -4,7 +4,7 @@ import JSON5 from "json5"; import env from "$/env.json"; import reflexEnvironment from "$/reflex.json"; import Cookies from "universal-cookie"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useLocation, useNavigate, diff --git a/reflex/app.py b/reflex/app.py index 38918170f70..001a04ce77b 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -239,10 +239,15 @@ def default_error_boundary(*children: Component, **props) -> Component: The default error_boundary, which is an ErrorBoundary. """ - return ErrorBoundary.create( - *children, - **props, - ) + from reflex.components.component import memo + + def memoized_error_boundary(): + return ErrorBoundary.create( + *children, + **props, + ) + + return Fragment.create(memo(memoized_error_boundary)()) @dataclasses.dataclass(frozen=True) diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index a8a7dbe4ec2..f773378df9b 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -395,11 +395,11 @@ def context_template( initialEvents, clientStorage, ) - return createElement( + return useMemo(() => createElement( EventLoopContext.Provider, {{ value: [addEvents, connectErrors] }}, children - ); + ), [addEvents, connectErrors, children]); }} export function StateProvider({{ children }}) {{