From cd7ccb365335ab18a8e78094d5c555a8e2d99f39 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 30 Jan 2025 12:09:29 -0800 Subject: [PATCH 01/10] disable react strict mode for event loop --- .../.templates/jinja/web/pages/_app.js.jinja2 | 14 +++---- reflex/.templates/web/utils/state.js | 14 ++++--- reflex/app.py | 10 +++-- reflex/components/base/strict_mode.py | 10 +++++ reflex/utils/prerequisites.py | 1 - tests/units/test_app.py | 38 +++++++++++++++---- tests/units/test_prerequisites.py | 12 +++--- 7 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 reflex/components/base/strict_mode.py diff --git a/reflex/.templates/jinja/web/pages/_app.js.jinja2 b/reflex/.templates/jinja/web/pages/_app.js.jinja2 index 40e31dee6dc..ee3e245409c 100644 --- a/reflex/.templates/jinja/web/pages/_app.js.jinja2 +++ b/reflex/.templates/jinja/web/pages/_app.js.jinja2 @@ -38,13 +38,13 @@ export default function MyApp({ Component, pageProps }) { }, []); return ( - - - - - - - + + + + + + + ); } diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 93c664ef180..1eeb4e64aab 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -301,10 +301,7 @@ export const applyEvent = async (event, socket) => { // Send the event to the server. if (socket) { - socket.emit( - "event", - event, - ); + socket.emit("event", event); return true; } @@ -497,7 +494,7 @@ export const uploadFiles = async ( return false; } - const upload_ref_name = `__upload_controllers_${upload_id}` + const upload_ref_name = `__upload_controllers_${upload_id}`; if (refs[upload_ref_name]) { console.log("Upload already in progress for ", upload_id); @@ -833,6 +830,13 @@ export const useEventLoop = ( } })(); } + + // Cleanup function. + return () => { + if (socket.current) { + socket.current.disconnect(); + } + }; }); // localStorage event handling diff --git a/reflex/app.py b/reflex/app.py index 9fe0f299267..3ba1ff53f6f 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -54,6 +54,7 @@ from reflex.components.base.app_wrap import AppWrap from reflex.components.base.error_boundary import ErrorBoundary from reflex.components.base.fragment import Fragment +from reflex.components.base.strict_mode import StrictMode from reflex.components.component import ( Component, ComponentStyle, @@ -943,6 +944,12 @@ def get_compilation_time() -> str: # If a theme component was provided, wrap the app with it app_wrappers[(20, "Theme")] = self.theme + # Get the env mode. + config = get_config() + + if config.react_strict_mode: + app_wrappers[(200, "StrictMode")] = StrictMode.create() + should_compile = self._should_compile() for route in self._unevaluated_pages: @@ -977,9 +984,6 @@ def get_compilation_time() -> str: + adhoc_steps_without_executor, ) - # Get the env mode. - config = get_config() - # Store the compile results. compile_results = [] diff --git a/reflex/components/base/strict_mode.py b/reflex/components/base/strict_mode.py new file mode 100644 index 00000000000..46b01ad872c --- /dev/null +++ b/reflex/components/base/strict_mode.py @@ -0,0 +1,10 @@ +"""Module for the StrictMode component.""" + +from reflex.components.component import Component + + +class StrictMode(Component): + """A React strict mode component to enable strict mode for its children.""" + + library = "react" + tag = "StrictMode" diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 8330a315c98..e79963dc7a3 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -912,7 +912,6 @@ def _update_next_config( next_config = { "basePath": config.frontend_path or "", "compress": config.next_compression, - "reactStrictMode": config.react_strict_mode, "trailingSlash": True, "staticPageGenerationTimeout": config.static_page_generation_timeout, } diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 074e7f2ef61..cf49f9d5c1d 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -1276,12 +1276,23 @@ def compilable_app(tmp_path) -> Generator[tuple[App, Path], None, None]: yield app, web_dir -def test_app_wrap_compile_theme(compilable_app: tuple[App, Path]): +@pytest.mark.parametrize( + "react_strict_mode", + [True, False], +) +def test_app_wrap_compile_theme( + react_strict_mode: bool, compilable_app: tuple[App, Path], mocker +): """Test that the radix theme component wraps the app. Args: + react_strict_mode: Whether to use React Strict Mode. compilable_app: compilable_app fixture. + mocker: pytest mocker object. """ + conf = rx.Config(app_name="testing", react_strict_mode=react_strict_mode) + mocker.patch("reflex.config._get_config", return_value=conf) + app, web_dir = compilable_app app.theme = rx.theme(accent_color="plum") app._compile() @@ -1292,24 +1303,37 @@ def test_app_wrap_compile_theme(compilable_app: tuple[App, Path]): assert ( "function AppWrap({children}) {" "return (" - "" + + ("" if react_strict_mode else "") + + "" "" "" "{children}" "" "" "" - ")" + + ("" if react_strict_mode else "") + + ")" "}" ) in "".join(app_js_lines) -def test_app_wrap_priority(compilable_app: tuple[App, Path]): +@pytest.mark.parametrize( + "react_strict_mode", + [True, False], +) +def test_app_wrap_priority( + react_strict_mode: bool, compilable_app: tuple[App, Path], mocker +): """Test that the app wrap components are wrapped in the correct order. Args: + react_strict_mode: Whether to use React Strict Mode. compilable_app: compilable_app fixture. + mocker: pytest mocker object. """ + conf = rx.Config(app_name="testing", react_strict_mode=react_strict_mode) + mocker.patch("reflex.config._get_config", return_value=conf) + app, web_dir = compilable_app class Fragment1(Component): @@ -1341,8 +1365,7 @@ def page(): ] assert ( "function AppWrap({children}) {" - "return (" - "" + "return (" + ("" if react_strict_mode else "") + "" '' "" "" @@ -1352,8 +1375,7 @@ def page(): "" "" "" - "" - ")" + "" + ("" if react_strict_mode else "") + ")" "}" ) in "".join(app_js_lines) diff --git a/tests/units/test_prerequisites.py b/tests/units/test_prerequisites.py index 3bd0290772c..4723d864883 100644 --- a/tests/units/test_prerequisites.py +++ b/tests/units/test_prerequisites.py @@ -32,7 +32,7 @@ app_name="test", ), False, - 'module.exports = {basePath: "", compress: true, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 60};', + 'module.exports = {basePath: "", compress: true, trailingSlash: true, staticPageGenerationTimeout: 60};', ), ( Config( @@ -40,7 +40,7 @@ static_page_generation_timeout=30, ), False, - 'module.exports = {basePath: "", compress: true, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 30};', + 'module.exports = {basePath: "", compress: true, trailingSlash: true, staticPageGenerationTimeout: 30};', ), ( Config( @@ -48,7 +48,7 @@ next_compression=False, ), False, - 'module.exports = {basePath: "", compress: false, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 60};', + 'module.exports = {basePath: "", compress: false, trailingSlash: true, staticPageGenerationTimeout: 60};', ), ( Config( @@ -56,7 +56,7 @@ frontend_path="/test", ), False, - 'module.exports = {basePath: "/test", compress: true, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 60};', + 'module.exports = {basePath: "/test", compress: true, trailingSlash: true, staticPageGenerationTimeout: 60};', ), ( Config( @@ -65,14 +65,14 @@ next_compression=False, ), False, - 'module.exports = {basePath: "/test", compress: false, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 60};', + 'module.exports = {basePath: "/test", compress: false, trailingSlash: true, staticPageGenerationTimeout: 60};', ), ( Config( app_name="test", ), True, - 'module.exports = {basePath: "", compress: true, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 60, output: "export", distDir: "_static"};', + 'module.exports = {basePath: "", compress: true, trailingSlash: true, staticPageGenerationTimeout: 60, output: "export", distDir: "_static"};', ), ], ) From 95a0474963eefa2c19760bd21330b29c04d2076d Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 30 Jan 2025 14:18:59 -0800 Subject: [PATCH 02/10] oops --- reflex/.templates/web/utils/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 1eeb4e64aab..6a5e53c673e 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -837,7 +837,7 @@ export const useEventLoop = ( socket.current.disconnect(); } }; - }); + }, []); // localStorage event handling useEffect(() => { From 159d17cb53016c6ab63205935858825b2bdbe631 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Thu, 30 Jan 2025 14:21:19 -0800 Subject: [PATCH 03/10] pyi oui --- reflex/components/base/strict_mode.pyi | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 reflex/components/base/strict_mode.pyi diff --git a/reflex/components/base/strict_mode.pyi b/reflex/components/base/strict_mode.pyi new file mode 100644 index 00000000000..9005c022223 --- /dev/null +++ b/reflex/components/base/strict_mode.pyi @@ -0,0 +1,57 @@ +"""Stub file for reflex/components/base/strict_mode.py""" + +# ------------------- DO NOT EDIT ---------------------- +# This file was generated by `reflex/utils/pyi_generator.py`! +# ------------------------------------------------------ +from typing import Any, Dict, Optional, Union, overload + +from reflex.components.component import Component +from reflex.event import BASE_STATE, EventType +from reflex.style import Style +from reflex.vars.base import Var + +class StrictMode(Component): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_blur: Optional[EventType[[], BASE_STATE]] = None, + on_click: Optional[EventType[[], BASE_STATE]] = None, + on_context_menu: Optional[EventType[[], BASE_STATE]] = None, + on_double_click: Optional[EventType[[], BASE_STATE]] = None, + on_focus: Optional[EventType[[], BASE_STATE]] = None, + on_mount: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_down: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_move: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_out: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_over: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_up: Optional[EventType[[], BASE_STATE]] = None, + on_scroll: Optional[EventType[[], BASE_STATE]] = None, + on_unmount: Optional[EventType[[], BASE_STATE]] = None, + **props, + ) -> "StrictMode": + """Create the component. + + Args: + *children: The children of the component. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The props of the component. + + Returns: + The component. + """ + ... From 480bac98dfa1df331087444aae51b4daf0f482b3 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 31 Jan 2025 14:47:28 -0800 Subject: [PATCH 04/10] separate socket connection from event loop --- reflex/.templates/web/utils/state.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 6a5e53c673e..c1c46755595 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -805,12 +805,8 @@ export const useEventLoop = ( }; }, []); - // Main event loop. + // Handle socket connect/disconnect. useEffect(() => { - // Skip if the router is not ready. - if (!router.isReady) { - return; - } // only use websockets if state is present if (Object.keys(initialState).length > 1) { // Initialize the websocket connection. @@ -820,15 +816,9 @@ export const useEventLoop = ( dispatch, ["websocket"], setConnectErrors, - client_storage + client_storage, ); } - (async () => { - // Process all outstanding events. - while (event_queue.length > 0 && !event_processing) { - await processEvent(socket.current); - } - })(); } // Cleanup function. @@ -839,6 +829,20 @@ export const useEventLoop = ( }; }, []); + // Main event loop. + useEffect(() => { + // Skip if the router is not ready. + if (!router.isReady) { + return; + } + (async () => { + // Process all outstanding events. + while (event_queue.length > 0 && !event_processing) { + await processEvent(socket.current); + } + })(); + }); + // localStorage event handling useEffect(() => { const storage_to_state_map = {}; From ab23f7e86f2629bc8ad4ad25654e4a100801cf3c Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 31 Jan 2025 14:47:50 -0800 Subject: [PATCH 05/10] prettier state.js --- reflex/.templates/web/utils/state.js | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index c1c46755595..61f0d59a2a9 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -215,8 +215,8 @@ export const applyEvent = async (event, socket) => { a.href = eval?.( event.payload.url.replace( "getBackendURL(env.UPLOAD)", - `"${getBackendURL(env.UPLOAD)}"` - ) + `"${getBackendURL(env.UPLOAD)}"`, + ), ); } a.download = event.payload.filename; @@ -329,7 +329,7 @@ export const applyRestEvent = async (event, socket) => { event.payload.files, event.payload.upload_id, event.payload.on_upload_progress, - socket + socket, ); return false; } @@ -396,7 +396,7 @@ export const connect = async ( dispatch, transports, setConnectErrors, - client_storage = {} + client_storage = {}, ) => { // Get backend URL object from the endpoint. const endpoint = getBackendURL(EVENTURL); @@ -487,7 +487,7 @@ export const uploadFiles = async ( files, upload_id, on_upload_progress, - socket + socket, ) => { // return if there's no file to upload if (files === undefined || files.length === 0) { @@ -592,7 +592,7 @@ export const Event = ( name, payload = {}, event_actions = {}, - handler = null + handler = null, ) => { return { name, payload, handler, event_actions }; }; @@ -619,7 +619,7 @@ export const hydrateClientStorage = (client_storage) => { for (const state_key in client_storage.local_storage) { const options = client_storage.local_storage[state_key]; const local_storage_value = localStorage.getItem( - options.name || state_key + options.name || state_key, ); if (local_storage_value !== null) { client_storage_values[state_key] = local_storage_value; @@ -630,7 +630,7 @@ export const hydrateClientStorage = (client_storage) => { for (const state_key in client_storage.session_storage) { const session_options = client_storage.session_storage[state_key]; const session_storage_value = sessionStorage.getItem( - session_options.name || state_key + session_options.name || state_key, ); if (session_storage_value != null) { client_storage_values[state_key] = session_storage_value; @@ -655,7 +655,7 @@ export const hydrateClientStorage = (client_storage) => { const applyClientStorageDelta = (client_storage, delta) => { // find the main state and check for is_hydrated const unqualified_states = Object.keys(delta).filter( - (key) => key.split(".").length === 1 + (key) => key.split(".").length === 1, ); if (unqualified_states.length === 1) { const main_state = delta[unqualified_states[0]]; @@ -689,7 +689,7 @@ const applyClientStorageDelta = (client_storage, delta) => { const session_options = client_storage.session_storage[state_key]; sessionStorage.setItem( session_options.name || state_key, - delta[substate][key] + delta[substate][key], ); } } @@ -709,7 +709,7 @@ const applyClientStorageDelta = (client_storage, delta) => { export const useEventLoop = ( dispatch, initial_events = () => [], - client_storage = {} + client_storage = {}, ) => { const socket = useRef(null); const router = useRouter(); @@ -723,7 +723,7 @@ export const useEventLoop = ( event_actions = events.reduce( (acc, e) => ({ ...acc, ...e.event_actions }), - event_actions ?? {} + event_actions ?? {}, ); const _e = args.filter((o) => o?.preventDefault !== undefined)[0]; @@ -751,7 +751,7 @@ export const useEventLoop = ( debounce( combined_name, () => queueEvents(events, socket), - event_actions.debounce + event_actions.debounce, ); } else { queueEvents(events, socket); @@ -770,7 +770,7 @@ export const useEventLoop = ( query, asPath, }))(router), - })) + })), ); sentHydrate.current = true; } @@ -864,7 +864,7 @@ export const useEventLoop = ( vars[storage_to_state_map[e.key]] = e.newValue; const event = Event( `${state_name}.reflex___state____update_vars_internal_state.update_vars_internal`, - { vars: vars } + { vars: vars }, ); addEvents([event], e); } @@ -957,7 +957,7 @@ export const getRefValues = (refs) => { return refs.map((ref) => ref.current ? ref.current.value || ref.current.getAttribute("aria-valuenow") - : null + : null, ); }; From b9b34d33db39497405f3f63952b1e507d1a5e264 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Fri, 31 Jan 2025 15:11:00 -0800 Subject: [PATCH 06/10] disable react strict mode --- reflex/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/config.py b/reflex/config.py index f6992f8b50d..113af12e2bc 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -663,7 +663,7 @@ class Config: # pyright: ignore [reportIncompatibleVariableOverride] next_compression: bool = True # Whether to use React strict mode in nextJS - react_strict_mode: bool = True + react_strict_mode: bool = False # Additional frontend packages to install. frontend_packages: List[str] = [] From 12f6f98b798cb2a76d5dcdaee29e47c009a6adfe Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Fri, 31 Jan 2025 15:15:11 -0800 Subject: [PATCH 07/10] didn't work sadge --- reflex/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/config.py b/reflex/config.py index 113af12e2bc..f6992f8b50d 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -663,7 +663,7 @@ class Config: # pyright: ignore [reportIncompatibleVariableOverride] next_compression: bool = True # Whether to use React strict mode in nextJS - react_strict_mode: bool = False + react_strict_mode: bool = True # Additional frontend packages to install. frontend_packages: List[str] = [] From 38d2ef721dfecaf897096b305d3eb0d1956c967c Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 31 Jan 2025 15:33:34 -0800 Subject: [PATCH 08/10] socket connect/disconnect depends on new isBackendDisabled state --- reflex/.templates/web/utils/state.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 069e651f944..947c75c4b43 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -819,7 +819,7 @@ export const useEventLoop = ( // Handle socket connect/disconnect. useEffect(() => { - // only use websockets if state is present + // only use websockets if state is present and backend is not disabled (reflex cloud). if (Object.keys(initialState).length > 1 && !isBackendDisabled()) { // Initialize the websocket connection. if (!socket.current) { @@ -839,7 +839,7 @@ export const useEventLoop = ( socket.current.disconnect(); } }; - }, []); + }, [isBackendDisabled]); // Main event loop. useEffect(() => { From 4179ec9c49bf4032b8b0bf1f4d095ae7a29879e5 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 31 Jan 2025 16:22:00 -0800 Subject: [PATCH 09/10] only start the event loop when the socket is set or we're not stateful --- reflex/.templates/web/utils/state.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 947c75c4b43..d9eca3ab19d 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -839,7 +839,7 @@ export const useEventLoop = ( socket.current.disconnect(); } }; - }, [isBackendDisabled]); + }, []); // Main event loop. useEffect(() => { @@ -847,13 +847,15 @@ export const useEventLoop = ( if (!router.isReady) { return; } - (async () => { - // Process all outstanding events. - while (event_queue.length > 0 && !event_processing) { - await processEvent(socket.current); - } - })(); - }); + if (socket.current || !isStateful()) { + (async () => { + // Process all outstanding events. + while (event_queue.length > 0 && !event_processing) { + await processEvent(socket.current); + } + })(); + } + }, [socket]); // localStorage event handling useEffect(() => { From 59d50921580a9b18678edce8b4cea02e651d8643 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 31 Jan 2025 16:31:42 -0800 Subject: [PATCH 10/10] Always drain the queue unless backend is disabled --- reflex/.templates/web/utils/state.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index d9eca3ab19d..2f09ac2debd 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -844,18 +844,16 @@ export const useEventLoop = ( // Main event loop. useEffect(() => { // Skip if the router is not ready. - if (!router.isReady) { + if (!router.isReady || isBackendDisabled()) { return; } - if (socket.current || !isStateful()) { - (async () => { - // Process all outstanding events. - while (event_queue.length > 0 && !event_processing) { - await processEvent(socket.current); - } - })(); - } - }, [socket]); + (async () => { + // Process all outstanding events. + while (event_queue.length > 0 && !event_processing) { + await processEvent(socket.current); + } + })(); + }); // localStorage event handling useEffect(() => {