reflex 0.6.8a1__py3-none-any.whl → 0.7.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of reflex might be problematic. Click here for more details.

Files changed (155) hide show
  1. reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 +1 -1
  2. reflex/.templates/jinja/web/pages/_app.js.jinja2 +7 -7
  3. reflex/.templates/jinja/web/pages/utils.js.jinja2 +2 -2
  4. reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +1 -4
  5. reflex/.templates/web/utils/state.js +65 -36
  6. reflex/__init__.py +4 -17
  7. reflex/__init__.pyi +1 -2
  8. reflex/app.py +244 -115
  9. reflex/app_mixins/lifespan.py +9 -9
  10. reflex/app_mixins/middleware.py +6 -6
  11. reflex/app_module_for_backend.py +3 -7
  12. reflex/base.py +7 -7
  13. reflex/compiler/compiler.py +8 -0
  14. reflex/compiler/utils.py +35 -6
  15. reflex/components/base/bare.py +1 -1
  16. reflex/components/base/error_boundary.py +2 -1
  17. reflex/components/base/error_boundary.pyi +2 -1
  18. reflex/components/base/meta.py +2 -2
  19. reflex/components/base/strict_mode.py +10 -0
  20. reflex/components/base/strict_mode.pyi +57 -0
  21. reflex/components/component.py +38 -77
  22. reflex/components/core/banner.py +83 -4
  23. reflex/components/core/banner.pyi +86 -0
  24. reflex/components/core/breakpoints.py +3 -1
  25. reflex/components/core/client_side_routing.py +1 -1
  26. reflex/components/core/client_side_routing.pyi +1 -1
  27. reflex/components/core/cond.py +9 -10
  28. reflex/components/core/debounce.py +1 -1
  29. reflex/components/core/foreach.py +23 -3
  30. reflex/components/core/html.py +1 -1
  31. reflex/components/core/match.py +5 -5
  32. reflex/components/core/sticky.py +160 -0
  33. reflex/components/core/sticky.pyi +449 -0
  34. reflex/components/core/upload.py +2 -2
  35. reflex/components/datadisplay/code.py +5 -14
  36. reflex/components/datadisplay/dataeditor.py +7 -4
  37. reflex/components/datadisplay/logo.py +13 -8
  38. reflex/components/datadisplay/shiki_code_block.py +14 -9
  39. reflex/components/dynamic.py +22 -3
  40. reflex/components/el/constants/reflex.py +1 -1
  41. reflex/components/el/element.py +1 -1
  42. reflex/components/el/elements/forms.py +4 -4
  43. reflex/components/el/elements/forms.pyi +4 -4
  44. reflex/components/lucide/icon.py +46 -8
  45. reflex/components/lucide/icon.pyi +54 -0
  46. reflex/components/markdown/markdown.py +10 -8
  47. reflex/components/moment/moment.py +2 -2
  48. reflex/components/next/image.py +16 -4
  49. reflex/components/next/image.pyi +4 -2
  50. reflex/components/next/link.py +1 -1
  51. reflex/components/plotly/plotly.py +5 -5
  52. reflex/components/props.py +3 -3
  53. reflex/components/radix/__init__.pyi +1 -1
  54. reflex/components/radix/primitives/accordion.py +9 -5
  55. reflex/components/radix/primitives/accordion.pyi +3 -1
  56. reflex/components/radix/primitives/drawer.py +5 -2
  57. reflex/components/radix/primitives/drawer.pyi +4 -4
  58. reflex/components/radix/primitives/form.pyi +6 -6
  59. reflex/components/radix/primitives/progress.py +1 -1
  60. reflex/components/radix/primitives/slider.py +1 -1
  61. reflex/components/radix/themes/color_mode.py +11 -9
  62. reflex/components/radix/themes/components/alert_dialog.py +3 -0
  63. reflex/components/radix/themes/components/card.py +1 -1
  64. reflex/components/radix/themes/components/card.pyi +1 -1
  65. reflex/components/radix/themes/components/context_menu.py +5 -0
  66. reflex/components/radix/themes/components/dialog.py +3 -0
  67. reflex/components/radix/themes/components/dropdown_menu.py +5 -0
  68. reflex/components/radix/themes/components/hover_card.py +3 -0
  69. reflex/components/radix/themes/components/icon_button.py +2 -2
  70. reflex/components/radix/themes/components/icon_button.pyi +1 -0
  71. reflex/components/radix/themes/components/popover.py +3 -0
  72. reflex/components/radix/themes/components/radio_cards.py +2 -0
  73. reflex/components/radix/themes/components/radio_group.py +1 -1
  74. reflex/components/radix/themes/components/select.py +3 -0
  75. reflex/components/radix/themes/components/tabs.py +3 -0
  76. reflex/components/radix/themes/components/text_area.py +12 -0
  77. reflex/components/radix/themes/components/text_area.pyi +2 -0
  78. reflex/components/radix/themes/components/text_field.py +1 -1
  79. reflex/components/radix/themes/components/tooltip.py +3 -1
  80. reflex/components/radix/themes/components/tooltip.pyi +1 -0
  81. reflex/components/radix/themes/layout/__init__.pyi +1 -1
  82. reflex/components/radix/themes/layout/list.py +2 -2
  83. reflex/components/radix/themes/layout/stack.py +2 -2
  84. reflex/components/radix/themes/typography/link.py +1 -1
  85. reflex/components/radix/themes/typography/text.py +2 -2
  86. reflex/components/react_player/react_player.py +1 -1
  87. reflex/components/recharts/__init__.py +2 -0
  88. reflex/components/recharts/__init__.pyi +2 -0
  89. reflex/components/recharts/charts.py +15 -15
  90. reflex/components/recharts/general.py +19 -4
  91. reflex/components/recharts/general.pyi +55 -4
  92. reflex/components/recharts/polar.py +2 -2
  93. reflex/components/recharts/recharts.py +4 -4
  94. reflex/components/sonner/toast.py +15 -13
  95. reflex/components/sonner/toast.pyi +6 -6
  96. reflex/components/suneditor/editor.py +6 -4
  97. reflex/components/suneditor/editor.pyi +2 -2
  98. reflex/components/tags/iter_tag.py +3 -3
  99. reflex/components/tags/tag.py +25 -3
  100. reflex/config.py +48 -20
  101. reflex/constants/__init__.py +1 -0
  102. reflex/constants/base.py +4 -1
  103. reflex/constants/compiler.py +5 -2
  104. reflex/constants/config.py +8 -1
  105. reflex/constants/installer.py +9 -9
  106. reflex/constants/style.py +1 -1
  107. reflex/custom_components/custom_components.py +9 -7
  108. reflex/event.py +137 -163
  109. reflex/experimental/__init__.py +19 -11
  110. reflex/experimental/client_state.py +53 -28
  111. reflex/experimental/hooks.py +5 -5
  112. reflex/experimental/layout.py +8 -5
  113. reflex/experimental/layout.pyi +1 -1
  114. reflex/experimental/misc.py +3 -3
  115. reflex/istate/wrappers.py +1 -1
  116. reflex/middleware/hydrate_middleware.py +2 -2
  117. reflex/model.py +11 -6
  118. reflex/page.py +3 -3
  119. reflex/reflex.py +90 -19
  120. reflex/route.py +1 -1
  121. reflex/state.py +358 -401
  122. reflex/style.py +27 -3
  123. reflex/testing.py +34 -39
  124. reflex/utils/build.py +6 -2
  125. reflex/utils/codespaces.py +1 -4
  126. reflex/utils/compat.py +6 -5
  127. reflex/utils/console.py +52 -21
  128. reflex/utils/exceptions.py +76 -26
  129. reflex/utils/exec.py +69 -74
  130. reflex/utils/export.py +6 -1
  131. reflex/utils/format.py +7 -39
  132. reflex/utils/imports.py +2 -2
  133. reflex/utils/lazy_loader.py +7 -1
  134. reflex/utils/path_ops.py +28 -14
  135. reflex/utils/prerequisites.py +324 -65
  136. reflex/utils/processes.py +45 -32
  137. reflex/utils/pyi_generator.py +30 -25
  138. reflex/utils/registry.py +4 -4
  139. reflex/utils/serializers.py +1 -1
  140. reflex/utils/telemetry.py +5 -4
  141. reflex/utils/types.py +42 -18
  142. reflex/vars/base.py +650 -333
  143. reflex/vars/datetime.py +6 -7
  144. reflex/vars/dep_tracking.py +344 -0
  145. reflex/vars/function.py +11 -5
  146. reflex/vars/number.py +31 -43
  147. reflex/vars/object.py +63 -62
  148. reflex/vars/sequence.py +79 -67
  149. {reflex-0.6.8a1.dist-info → reflex-0.7.0a1.dist-info}/METADATA +7 -10
  150. {reflex-0.6.8a1.dist-info → reflex-0.7.0a1.dist-info}/RECORD +153 -150
  151. {reflex-0.6.8a1.dist-info → reflex-0.7.0a1.dist-info}/WHEEL +1 -1
  152. reflex/experimental/assets.py +0 -37
  153. reflex/proxy.py +0 -119
  154. {reflex-0.6.8a1.dist-info → reflex-0.7.0a1.dist-info}/LICENSE +0 -0
  155. {reflex-0.6.8a1.dist-info → reflex-0.7.0a1.dist-info}/entry_points.txt +0 -0
reflex/app.py CHANGED
@@ -27,6 +27,7 @@ from typing import (
27
27
  Dict,
28
28
  Generic,
29
29
  List,
30
+ MutableMapping,
30
31
  Optional,
31
32
  Set,
32
33
  Type,
@@ -53,21 +54,28 @@ from reflex.compiler.compiler import ExecutorSafeFunctions, compile_theme
53
54
  from reflex.components.base.app_wrap import AppWrap
54
55
  from reflex.components.base.error_boundary import ErrorBoundary
55
56
  from reflex.components.base.fragment import Fragment
57
+ from reflex.components.base.strict_mode import StrictMode
56
58
  from reflex.components.component import (
57
59
  Component,
58
60
  ComponentStyle,
59
61
  evaluate_style_namespaces,
60
62
  )
61
- from reflex.components.core.banner import connection_pulser, connection_toaster
63
+ from reflex.components.core.banner import (
64
+ backend_disabled,
65
+ connection_pulser,
66
+ connection_toaster,
67
+ )
62
68
  from reflex.components.core.breakpoints import set_breakpoints
63
69
  from reflex.components.core.client_side_routing import (
64
70
  Default404Page,
65
71
  wait_for_client_redirect,
66
72
  )
73
+ from reflex.components.core.sticky import sticky
67
74
  from reflex.components.core.upload import Upload, get_upload_dir
68
75
  from reflex.components.radix import themes
69
76
  from reflex.config import environment, get_config
70
77
  from reflex.event import (
78
+ _EVENT_FIELDS,
71
79
  BASE_STATE,
72
80
  Event,
73
81
  EventHandler,
@@ -144,7 +152,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
144
152
  position="top-center",
145
153
  id="backend_error",
146
154
  style={"width": "500px"},
147
- ) # type: ignore
155
+ )
148
156
  else:
149
157
  error_message.insert(0, "An error occurred.")
150
158
  return window_alert("\n".join(error_message))
@@ -156,9 +164,12 @@ def default_overlay_component() -> Component:
156
164
  Returns:
157
165
  The default overlay_component, which is a connection_modal.
158
166
  """
167
+ config = get_config()
168
+
159
169
  return Fragment.create(
160
170
  connection_pulser(),
161
171
  connection_toaster(),
172
+ *([backend_disabled()] if config.is_reflex_cloud else []),
162
173
  *codespaces.codespaces_auto_redirect(),
163
174
  )
164
175
 
@@ -250,36 +261,36 @@ class App(MiddlewareMixin, LifespanMixin):
250
261
  # Attributes to add to the html root tag of every page.
251
262
  html_custom_attrs: Optional[Dict[str, str]] = None
252
263
 
253
- # A map from a route to an unevaluated page. PRIVATE.
254
- unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
264
+ # A map from a route to an unevaluated page.
265
+ _unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
255
266
  default_factory=dict
256
267
  )
257
268
 
258
- # A map from a page route to the component to render. Users should use `add_page`. PRIVATE.
259
- pages: Dict[str, Component] = dataclasses.field(default_factory=dict)
269
+ # A map from a page route to the component to render. Users should use `add_page`.
270
+ _pages: Dict[str, Component] = dataclasses.field(default_factory=dict)
260
271
 
261
- # The backend API object. PRIVATE.
262
- api: FastAPI = None # type: ignore
272
+ # The backend API object.
273
+ _api: FastAPI | None = None
263
274
 
264
- # The state class to use for the app. PRIVATE.
265
- state: Optional[Type[BaseState]] = None
275
+ # The state class to use for the app.
276
+ _state: Optional[Type[BaseState]] = None
266
277
 
267
278
  # Class to manage many client states.
268
279
  _state_manager: Optional[StateManager] = None
269
280
 
270
- # Mapping from a route to event handlers to trigger when the page loads. PRIVATE.
271
- load_events: Dict[str, List[IndividualEventType[[], Any]]] = dataclasses.field(
281
+ # Mapping from a route to event handlers to trigger when the page loads.
282
+ _load_events: Dict[str, List[IndividualEventType[[], Any]]] = dataclasses.field(
272
283
  default_factory=dict
273
284
  )
274
285
 
275
- # Admin dashboard to view and manage the database. PRIVATE.
286
+ # Admin dashboard to view and manage the database.
276
287
  admin_dash: Optional[AdminDash] = None
277
288
 
278
- # The async server name space. PRIVATE.
279
- event_namespace: Optional[EventNamespace] = None
289
+ # The async server name space.
290
+ _event_namespace: Optional[EventNamespace] = None
280
291
 
281
- # Background tasks that are currently running. PRIVATE.
282
- background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
292
+ # Background tasks that are currently running.
293
+ _background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
283
294
 
284
295
  # Frontend Error Handler Function
285
296
  frontend_exception_handler: Callable[[Exception], None] = (
@@ -291,6 +302,24 @@ class App(MiddlewareMixin, LifespanMixin):
291
302
  [Exception], Union[EventSpec, List[EventSpec], None]
292
303
  ] = default_backend_exception_handler
293
304
 
305
+ @property
306
+ def api(self) -> FastAPI | None:
307
+ """Get the backend api.
308
+
309
+ Returns:
310
+ The backend api.
311
+ """
312
+ return self._api
313
+
314
+ @property
315
+ def event_namespace(self) -> EventNamespace | None:
316
+ """Get the event namespace.
317
+
318
+ Returns:
319
+ The event namespace.
320
+ """
321
+ return self._event_namespace
322
+
294
323
  def __post_init__(self):
295
324
  """Initialize the app.
296
325
 
@@ -310,7 +339,7 @@ class App(MiddlewareMixin, LifespanMixin):
310
339
  set_breakpoints(self.style.pop("breakpoints"))
311
340
 
312
341
  # Set up the API.
313
- self.api = FastAPI(lifespan=self._run_lifespan_tasks)
342
+ self._api = FastAPI(lifespan=self._run_lifespan_tasks)
314
343
  self._add_cors()
315
344
  self._add_default_endpoints()
316
345
 
@@ -331,16 +360,10 @@ class App(MiddlewareMixin, LifespanMixin):
331
360
 
332
361
  self.register_lifespan_task(windows_hot_reload_lifespan_hack)
333
362
 
334
- # Enable proxying to frontend server.
335
- if not environment.REFLEX_BACKEND_ONLY.get():
336
- from reflex.proxy import proxy_middleware
337
-
338
- self.register_lifespan_task(proxy_middleware)
339
-
340
363
  def _enable_state(self) -> None:
341
364
  """Enable state for the app."""
342
- if not self.state:
343
- self.state = State
365
+ if not self._state:
366
+ self._state = State
344
367
  self._setup_state()
345
368
 
346
369
  def _setup_state(self) -> None:
@@ -349,13 +372,13 @@ class App(MiddlewareMixin, LifespanMixin):
349
372
  Raises:
350
373
  RuntimeError: If the socket server is invalid.
351
374
  """
352
- if not self.state:
375
+ if not self._state:
353
376
  return
354
377
 
355
378
  config = get_config()
356
379
 
357
380
  # Set up the state manager.
358
- self._state_manager = StateManager.create(state=self.state)
381
+ self._state_manager = StateManager.create(state=self._state)
359
382
 
360
383
  # Set up the Socket.IO AsyncServer.
361
384
  if not self.sio:
@@ -386,12 +409,42 @@ class App(MiddlewareMixin, LifespanMixin):
386
409
  namespace = config.get_event_namespace()
387
410
 
388
411
  # Create the event namespace and attach the main app. Not related to any paths.
389
- self.event_namespace = EventNamespace(namespace, self)
412
+ self._event_namespace = EventNamespace(namespace, self)
390
413
 
391
414
  # Register the event namespace with the socket.
392
415
  self.sio.register_namespace(self.event_namespace)
393
416
  # Mount the socket app with the API.
394
- self.api.mount(str(constants.Endpoint.EVENT), socket_app)
417
+ if self.api:
418
+
419
+ class HeaderMiddleware:
420
+ def __init__(self, app: ASGIApp):
421
+ self.app = app
422
+
423
+ async def __call__(
424
+ self, scope: MutableMapping[str, Any], receive: Any, send: Callable
425
+ ):
426
+ original_send = send
427
+
428
+ async def modified_send(message: dict):
429
+ if message["type"] == "websocket.accept":
430
+ if scope.get("subprotocols"):
431
+ # The following *does* say "subprotocol" instead of "subprotocols", intentionally.
432
+ message["subprotocol"] = scope["subprotocols"][0]
433
+
434
+ headers = dict(message.get("headers", []))
435
+ header_key = b"sec-websocket-protocol"
436
+ if subprotocol := headers.get(header_key):
437
+ message["headers"] = [
438
+ *message.get("headers", []),
439
+ (header_key, subprotocol),
440
+ ]
441
+
442
+ return await original_send(message)
443
+
444
+ return await self.app(scope, receive, modified_send)
445
+
446
+ socket_app_with_headers = HeaderMiddleware(socket_app)
447
+ self.api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers)
395
448
 
396
449
  # Check the exception handlers
397
450
  self._validate_exception_handlers()
@@ -402,24 +455,35 @@ class App(MiddlewareMixin, LifespanMixin):
402
455
  Returns:
403
456
  The string representation of the app.
404
457
  """
405
- return f"<App state={self.state.__name__ if self.state else None}>"
458
+ return f"<App state={self._state.__name__ if self._state else None}>"
406
459
 
407
460
  def __call__(self) -> FastAPI:
408
461
  """Run the backend api instance.
409
462
 
463
+ Raises:
464
+ ValueError: If the app has not been initialized.
465
+
410
466
  Returns:
411
467
  The backend api.
412
468
  """
469
+ if not self.api:
470
+ raise ValueError("The app has not been initialized.")
413
471
  return self.api
414
472
 
415
473
  def _add_default_endpoints(self):
416
474
  """Add default api endpoints (ping)."""
417
475
  # To test the server.
476
+ if not self.api:
477
+ return
478
+
418
479
  self.api.get(str(constants.Endpoint.PING))(ping)
419
480
  self.api.get(str(constants.Endpoint.HEALTH))(health)
420
481
 
421
482
  def _add_optional_endpoints(self):
422
483
  """Add optional api endpoints (_upload)."""
484
+ if not self.api:
485
+ return
486
+
423
487
  if Upload.is_used:
424
488
  # To upload files.
425
489
  self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
@@ -437,6 +501,8 @@ class App(MiddlewareMixin, LifespanMixin):
437
501
 
438
502
  def _add_cors(self):
439
503
  """Add CORS middleware to the app."""
504
+ if not self.api:
505
+ return
440
506
  self.api.add_middleware(
441
507
  cors.CORSMiddleware,
442
508
  allow_credentials=True,
@@ -468,14 +534,8 @@ class App(MiddlewareMixin, LifespanMixin):
468
534
 
469
535
  Returns:
470
536
  The generated component.
471
-
472
- Raises:
473
- exceptions.MatchTypeError: If the return types of match cases in rx.match are different.
474
537
  """
475
- try:
476
- return component if isinstance(component, Component) else component()
477
- except exceptions.MatchTypeError:
478
- raise
538
+ return component if isinstance(component, Component) else component()
479
539
 
480
540
  def add_page(
481
541
  self,
@@ -532,13 +592,13 @@ class App(MiddlewareMixin, LifespanMixin):
532
592
  # Check if the route given is valid
533
593
  verify_route_validity(route)
534
594
 
535
- if route in self.unevaluated_pages and environment.RELOAD_CONFIG.is_set():
595
+ if route in self._unevaluated_pages and environment.RELOAD_CONFIG.is_set():
536
596
  # when the app is reloaded(typically for app harness tests), we should maintain
537
597
  # the latest render function of a route.This applies typically to decorated pages
538
598
  # since they are only added when app._compile is called.
539
- self.unevaluated_pages.pop(route)
599
+ self._unevaluated_pages.pop(route)
540
600
 
541
- if route in self.unevaluated_pages:
601
+ if route in self._unevaluated_pages:
542
602
  route_name = (
543
603
  f"`{route}` or `/`"
544
604
  if route == constants.PageNames.INDEX_ROUTE
@@ -551,15 +611,15 @@ class App(MiddlewareMixin, LifespanMixin):
551
611
 
552
612
  # Setup dynamic args for the route.
553
613
  # this state assignment is only required for tests using the deprecated state kwarg for App
554
- state = self.state if self.state else State
614
+ state = self._state if self._state else State
555
615
  state.setup_dynamic_args(get_route_args(route))
556
616
 
557
617
  if on_load:
558
- self.load_events[route] = (
618
+ self._load_events[route] = (
559
619
  on_load if isinstance(on_load, list) else [on_load]
560
620
  )
561
621
 
562
- self.unevaluated_pages[route] = UnevaluatedPage(
622
+ self._unevaluated_pages[route] = UnevaluatedPage(
563
623
  component=component,
564
624
  route=route,
565
625
  title=title,
@@ -569,14 +629,15 @@ class App(MiddlewareMixin, LifespanMixin):
569
629
  meta=meta,
570
630
  )
571
631
 
572
- def _compile_page(self, route: str):
632
+ def _compile_page(self, route: str, save_page: bool = True):
573
633
  """Compile a page.
574
634
 
575
635
  Args:
576
636
  route: The route of the page to compile.
637
+ save_page: If True, the compiled page is saved to self._pages.
577
638
  """
578
639
  component, enable_state = compiler.compile_unevaluated_page(
579
- route, self.unevaluated_pages[route], self.state, self.style, self.theme
640
+ route, self._unevaluated_pages[route], self._state, self.style, self.theme
580
641
  )
581
642
 
582
643
  if enable_state:
@@ -584,7 +645,8 @@ class App(MiddlewareMixin, LifespanMixin):
584
645
 
585
646
  # Add the page.
586
647
  self._check_routes_conflict(route)
587
- self.pages[route] = component
648
+ if save_page:
649
+ self._pages[route] = component
588
650
 
589
651
  def get_load_events(self, route: str) -> list[IndividualEventType[[], Any]]:
590
652
  """Get the load events for a route.
@@ -598,7 +660,7 @@ class App(MiddlewareMixin, LifespanMixin):
598
660
  route = route.lstrip("/")
599
661
  if route == "":
600
662
  route = constants.PageNames.INDEX_ROUTE
601
- return self.load_events.get(route, [])
663
+ return self._load_events.get(route, [])
602
664
 
603
665
  def _check_routes_conflict(self, new_route: str):
604
666
  """Verify if there is any conflict between the new route and any existing route.
@@ -622,10 +684,13 @@ class App(MiddlewareMixin, LifespanMixin):
622
684
  constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
623
685
  constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
624
686
  )
625
- for route in self.pages:
687
+ for route in self._pages:
626
688
  replaced_route = replace_brackets_with_keywords(route)
627
689
  for rw, r, nr in zip(
628
- replaced_route.split("/"), route.split("/"), new_route.split("/")
690
+ replaced_route.split("/"),
691
+ route.split("/"),
692
+ new_route.split("/"),
693
+ strict=False,
629
694
  ):
630
695
  if rw in segments and r != nr:
631
696
  # If the slugs in the segments of both routes are not the same, then the route is invalid
@@ -656,8 +721,8 @@ class App(MiddlewareMixin, LifespanMixin):
656
721
  Args:
657
722
  component: The component to display at the page.
658
723
  title: The title of the page.
659
- description: The description of the page.
660
724
  image: The image to display on the page.
725
+ description: The description of the page.
661
726
  on_load: The event handler(s) that will be called each time the page load.
662
727
  meta: The metadata of the page.
663
728
  """
@@ -680,6 +745,9 @@ class App(MiddlewareMixin, LifespanMixin):
680
745
  def _setup_admin_dash(self):
681
746
  """Setup the admin dash."""
682
747
  # Get the admin dash.
748
+ if not self.api:
749
+ return
750
+
683
751
  admin_dash = self.admin_dash
684
752
 
685
753
  if admin_dash and admin_dash.models:
@@ -721,7 +789,7 @@ class App(MiddlewareMixin, LifespanMixin):
721
789
  frontend_packages = get_config().frontend_packages
722
790
  _frontend_packages = []
723
791
  for package in frontend_packages:
724
- if package in (get_config().tailwind or {}).get("plugins", []): # type: ignore
792
+ if package in (get_config().tailwind or {}).get("plugins", []):
725
793
  console.warn(
726
794
  f"Tailwind packages are inferred from 'plugins', remove `{package}` from `frontend_packages`"
727
795
  )
@@ -784,10 +852,10 @@ class App(MiddlewareMixin, LifespanMixin):
784
852
 
785
853
  def _setup_overlay_component(self):
786
854
  """If a State is not used and no overlay_component is specified, do not render the connection modal."""
787
- if self.state is None and self.overlay_component is default_overlay_component:
855
+ if self._state is None and self.overlay_component is default_overlay_component:
788
856
  self.overlay_component = None
789
- for k, component in self.pages.items():
790
- self.pages[k] = self._add_overlay_to_component(component)
857
+ for k, component in self._pages.items():
858
+ self._pages[k] = self._add_overlay_to_component(component)
791
859
 
792
860
  def _add_error_boundary_to_component(self, component: Component) -> Component:
793
861
  if self.error_boundary is None:
@@ -799,14 +867,23 @@ class App(MiddlewareMixin, LifespanMixin):
799
867
 
800
868
  def _setup_error_boundary(self):
801
869
  """If a State is not used and no error_boundary is specified, do not render the error boundary."""
802
- if self.state is None and self.error_boundary is default_error_boundary:
870
+ if self._state is None and self.error_boundary is default_error_boundary:
803
871
  self.error_boundary = None
804
872
 
805
- for k, component in self.pages.items():
873
+ for k, component in self._pages.items():
806
874
  # Skip the 404 page
807
875
  if k == constants.Page404.SLUG:
808
876
  continue
809
- self.pages[k] = self._add_error_boundary_to_component(component)
877
+ self._pages[k] = self._add_error_boundary_to_component(component)
878
+
879
+ def _setup_sticky_badge(self):
880
+ """Add the sticky badge to the app."""
881
+ for k, component in self._pages.items():
882
+ # Would be nice to share single sticky_badge across all pages, but
883
+ # it bungles the StatefulComponent compile step.
884
+ sticky_badge = sticky()
885
+ sticky_badge._add_style_recursive({})
886
+ self._pages[k] = Fragment.create(sticky_badge, component)
810
887
 
811
888
  def _apply_decorated_pages(self):
812
889
  """Add @rx.page decorated pages to the app.
@@ -832,21 +909,27 @@ class App(MiddlewareMixin, LifespanMixin):
832
909
  Raises:
833
910
  VarDependencyError: When a computed var has an invalid dependency.
834
911
  """
835
- if not self.state:
912
+ if not self._state:
836
913
  return
837
914
 
838
915
  if not state:
839
- state = self.state
916
+ state = self._state
840
917
 
841
918
  for var in state.computed_vars.values():
842
919
  if not var._cache:
843
920
  continue
844
921
  deps = var._deps(objclass=state)
845
- for dep in deps:
846
- if dep not in state.vars and dep not in state.backend_vars:
847
- raise exceptions.VarDependencyError(
848
- f"ComputedVar {var._js_expr} on state {state.__name__} has an invalid dependency {dep}"
849
- )
922
+ for state_name, dep_set in deps.items():
923
+ state_cls = (
924
+ state.get_root_state().get_class_substate(state_name)
925
+ if state_name != state.get_full_name()
926
+ else state
927
+ )
928
+ for dep in dep_set:
929
+ if dep not in state_cls.vars and dep not in state_cls.backend_vars:
930
+ raise exceptions.VarDependencyError(
931
+ f"ComputedVar {var._js_expr} on state {state.__name__} has an invalid dependency {state_name}.{dep}"
932
+ )
850
933
 
851
934
  for substate in state.class_subclasses:
852
935
  self._validate_var_dependencies(substate)
@@ -862,13 +945,13 @@ class App(MiddlewareMixin, LifespanMixin):
862
945
  """
863
946
  from reflex.utils.exceptions import ReflexRuntimeError
864
947
 
865
- self.pages = {}
948
+ self._pages = {}
866
949
 
867
950
  def get_compilation_time() -> str:
868
951
  return str(datetime.now().time()).split(".")[0]
869
952
 
870
953
  # Render a default 404 page if the user didn't supply one
871
- if constants.Page404.SLUG not in self.unevaluated_pages:
954
+ if constants.Page404.SLUG not in self._unevaluated_pages:
872
955
  self.add_page(route=constants.Page404.SLUG)
873
956
 
874
957
  # Fix up the style.
@@ -884,19 +967,23 @@ class App(MiddlewareMixin, LifespanMixin):
884
967
  # If a theme component was provided, wrap the app with it
885
968
  app_wrappers[(20, "Theme")] = self.theme
886
969
 
887
- for route in self.unevaluated_pages:
888
- console.debug(f"Evaluating page: {route}")
889
- self._compile_page(route)
970
+ # Get the env mode.
971
+ config = get_config()
890
972
 
891
- # Add the optional endpoints (_upload)
892
- self._add_optional_endpoints()
973
+ if config.react_strict_mode:
974
+ app_wrappers[(200, "StrictMode")] = StrictMode.create()
893
975
 
894
- if not self._should_compile():
895
- return
976
+ should_compile = self._should_compile()
896
977
 
897
- self._validate_var_dependencies()
898
- self._setup_overlay_component()
899
- self._setup_error_boundary()
978
+ if not should_compile:
979
+ for route in self._unevaluated_pages:
980
+ console.debug(f"Evaluating page: {route}")
981
+ self._compile_page(route, save_page=should_compile)
982
+
983
+ # Add the optional endpoints (_upload)
984
+ self._add_optional_endpoints()
985
+
986
+ return
900
987
 
901
988
  # Create a progress bar.
902
989
  progress = Progress(
@@ -906,18 +993,32 @@ class App(MiddlewareMixin, LifespanMixin):
906
993
  )
907
994
 
908
995
  # try to be somewhat accurate - but still not 100%
909
- adhoc_steps_without_executor = 6
996
+ adhoc_steps_without_executor = 7
910
997
  fixed_pages_within_executor = 5
911
998
  progress.start()
912
999
  task = progress.add_task(
913
1000
  f"[{get_compilation_time()}] Compiling:",
914
- total=len(self.pages)
1001
+ total=len(self._pages)
1002
+ + (len(self._unevaluated_pages) * 2)
915
1003
  + fixed_pages_within_executor
916
1004
  + adhoc_steps_without_executor,
917
1005
  )
918
1006
 
919
- # Get the env mode.
920
- config = get_config()
1007
+ for route in self._unevaluated_pages:
1008
+ console.debug(f"Evaluating page: {route}")
1009
+ self._compile_page(route, save_page=should_compile)
1010
+ progress.advance(task)
1011
+
1012
+ # Add the optional endpoints (_upload)
1013
+ self._add_optional_endpoints()
1014
+
1015
+ self._validate_var_dependencies()
1016
+ self._setup_overlay_component()
1017
+ self._setup_error_boundary()
1018
+ if config.show_built_with_reflex:
1019
+ self._setup_sticky_badge()
1020
+
1021
+ progress.advance(task)
921
1022
 
922
1023
  # Store the compile results.
923
1024
  compile_results = []
@@ -930,7 +1031,7 @@ class App(MiddlewareMixin, LifespanMixin):
930
1031
 
931
1032
  # This has to happen before compiling stateful components as that
932
1033
  # prevents recursive functions from reaching all components.
933
- for component in self.pages.values():
1034
+ for component in self._pages.values():
934
1035
  # Add component._get_all_imports() to all_imports.
935
1036
  all_imports.update(component._get_all_imports())
936
1037
 
@@ -945,12 +1046,12 @@ class App(MiddlewareMixin, LifespanMixin):
945
1046
  stateful_components_path,
946
1047
  stateful_components_code,
947
1048
  page_components,
948
- ) = compiler.compile_stateful_components(self.pages.values())
1049
+ ) = compiler.compile_stateful_components(self._pages.values())
949
1050
 
950
1051
  progress.advance(task)
951
1052
 
952
1053
  # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
953
- if code_uses_state_contexts(stateful_components_code) and self.state is None:
1054
+ if code_uses_state_contexts(stateful_components_code) and self._state is None:
954
1055
  raise ReflexRuntimeError(
955
1056
  "To access rx.State in frontend components, at least one "
956
1057
  "subclass of rx.State must be defined in the app."
@@ -964,7 +1065,7 @@ class App(MiddlewareMixin, LifespanMixin):
964
1065
  compiler.compile_document_root(
965
1066
  self.head_components,
966
1067
  html_lang=self.html_lang,
967
- html_custom_attrs=self.html_custom_attrs, # type: ignore
1068
+ html_custom_attrs=self.html_custom_attrs, # pyright: ignore [reportArgumentType]
968
1069
  )
969
1070
  )
970
1071
 
@@ -987,20 +1088,20 @@ class App(MiddlewareMixin, LifespanMixin):
987
1088
  max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
988
1089
  )
989
1090
 
990
- for route, component in zip(self.pages, page_components):
1091
+ for route, component in zip(self._pages, page_components, strict=True):
991
1092
  ExecutorSafeFunctions.COMPONENTS[route] = component
992
1093
 
993
- ExecutorSafeFunctions.STATE = self.state
1094
+ ExecutorSafeFunctions.STATE = self._state
994
1095
 
995
1096
  with executor:
996
1097
  result_futures = []
997
1098
 
998
- def _submit_work(fn, *args, **kwargs):
1099
+ def _submit_work(fn: Callable, *args, **kwargs):
999
1100
  f = executor.submit(fn, *args, **kwargs)
1000
1101
  result_futures.append(f)
1001
1102
 
1002
1103
  # Compile the pre-compiled pages.
1003
- for route in self.pages:
1104
+ for route in self._pages:
1004
1105
  _submit_work(
1005
1106
  ExecutorSafeFunctions.compile_page,
1006
1107
  route,
@@ -1035,7 +1136,7 @@ class App(MiddlewareMixin, LifespanMixin):
1035
1136
 
1036
1137
  # Compile the contexts.
1037
1138
  compile_results.append(
1038
- compiler.compile_contexts(self.state, self.theme),
1139
+ compiler.compile_contexts(self._state, self.theme),
1039
1140
  )
1040
1141
  if self.theme is not None:
1041
1142
  # Fix #2992 by removing the top-level appearance prop
@@ -1157,9 +1258,9 @@ class App(MiddlewareMixin, LifespanMixin):
1157
1258
  )
1158
1259
 
1159
1260
  task = asyncio.create_task(_coro())
1160
- self.background_tasks.add(task)
1261
+ self._background_tasks.add(task)
1161
1262
  # Clean up task from background_tasks set when complete.
1162
- task.add_done_callback(self.background_tasks.discard)
1263
+ task.add_done_callback(self._background_tasks.discard)
1163
1264
  return task
1164
1265
 
1165
1266
  def _validate_exception_handlers(self):
@@ -1169,11 +1270,11 @@ class App(MiddlewareMixin, LifespanMixin):
1169
1270
  ValueError: If the custom exception handlers are invalid.
1170
1271
 
1171
1272
  """
1172
- FRONTEND_ARG_SPEC = {
1273
+ frontend_arg_spec = {
1173
1274
  "exception": Exception,
1174
1275
  }
1175
1276
 
1176
- BACKEND_ARG_SPEC = {
1277
+ backend_arg_spec = {
1177
1278
  "exception": Exception,
1178
1279
  }
1179
1280
 
@@ -1181,9 +1282,10 @@ class App(MiddlewareMixin, LifespanMixin):
1181
1282
  ["frontend", "backend"],
1182
1283
  [self.frontend_exception_handler, self.backend_exception_handler],
1183
1284
  [
1184
- FRONTEND_ARG_SPEC,
1185
- BACKEND_ARG_SPEC,
1285
+ frontend_arg_spec,
1286
+ backend_arg_spec,
1186
1287
  ],
1288
+ strict=True,
1187
1289
  ):
1188
1290
  if hasattr(handler_fn, "__name__"):
1189
1291
  _fn_name = handler_fn.__name__
@@ -1224,7 +1326,7 @@ class App(MiddlewareMixin, LifespanMixin):
1224
1326
  ):
1225
1327
  raise ValueError(
1226
1328
  f"Provided custom {handler_domain} exception handler `{_fn_name}` has the wrong argument order."
1227
- f"Expected `{required_arg}` as the {required_arg_index+1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`"
1329
+ f"Expected `{required_arg}` as the {required_arg_index + 1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`"
1228
1330
  )
1229
1331
 
1230
1332
  if not issubclass(arg_annotations[required_arg], Exception):
@@ -1325,15 +1427,14 @@ async def process(
1325
1427
  if app._process_background(state, event) is not None:
1326
1428
  # `final=True` allows the frontend send more events immediately.
1327
1429
  yield StateUpdate(final=True)
1328
- return
1329
-
1330
- # Process the event synchronously.
1331
- async for update in state._process(event):
1332
- # Postprocess the event.
1333
- update = await app._postprocess(state, event, update)
1334
-
1335
- # Yield the update.
1336
- yield update
1430
+ else:
1431
+ # Process the event synchronously.
1432
+ async for update in state._process(event):
1433
+ # Postprocess the event.
1434
+ update = await app._postprocess(state, event, update)
1435
+
1436
+ # Yield the update.
1437
+ yield update
1337
1438
  except Exception as ex:
1338
1439
  telemetry.send_error(ex, context="backend")
1339
1440
 
@@ -1528,16 +1629,20 @@ class EventNamespace(AsyncNamespace):
1528
1629
  self.sid_to_token = {}
1529
1630
  self.app = app
1530
1631
 
1531
- def on_connect(self, sid, environ):
1632
+ def on_connect(self, sid: str, environ: dict):
1532
1633
  """Event for when the websocket is connected.
1533
1634
 
1534
1635
  Args:
1535
1636
  sid: The Socket.IO session id.
1536
1637
  environ: The request information, including HTTP headers.
1537
1638
  """
1538
- pass
1639
+ subprotocol = environ.get("HTTP_SEC_WEBSOCKET_PROTOCOL")
1640
+ if subprotocol and subprotocol != constants.Reflex.VERSION:
1641
+ console.warn(
1642
+ f"Frontend version {subprotocol} for session {sid} does not match the backend version {constants.Reflex.VERSION}."
1643
+ )
1539
1644
 
1540
- def on_disconnect(self, sid):
1645
+ def on_disconnect(self, sid: str):
1541
1646
  """Event for when the websocket disconnects.
1542
1647
 
1543
1648
  Args:
@@ -1559,7 +1664,7 @@ class EventNamespace(AsyncNamespace):
1559
1664
  self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
1560
1665
  )
1561
1666
 
1562
- async def on_event(self, sid, data):
1667
+ async def on_event(self, sid: str, data: Any):
1563
1668
  """Event for receiving front-end websocket events.
1564
1669
 
1565
1670
  Raises:
@@ -1568,12 +1673,36 @@ class EventNamespace(AsyncNamespace):
1568
1673
  Args:
1569
1674
  sid: The Socket.IO session id.
1570
1675
  data: The event data.
1676
+
1677
+ Raises:
1678
+ EventDeserializationError: If the event data is not a dictionary.
1571
1679
  """
1572
1680
  fields = data
1573
- # Get the event.
1574
- event = Event(
1575
- **{k: v for k, v in fields.items() if k not in ("handler", "event_actions")}
1576
- )
1681
+
1682
+ if isinstance(fields, str):
1683
+ console.warn(
1684
+ "Received event data as a string. This generally should not happen and may indicate a bug."
1685
+ f" Event data: {fields}"
1686
+ )
1687
+ try:
1688
+ fields = json.loads(fields)
1689
+ except json.JSONDecodeError as ex:
1690
+ raise exceptions.EventDeserializationError(
1691
+ f"Failed to deserialize event data: {fields}."
1692
+ ) from ex
1693
+
1694
+ if not isinstance(fields, dict):
1695
+ raise exceptions.EventDeserializationError(
1696
+ f"Event data must be a dictionary, but received {fields} of type {type(fields)}."
1697
+ )
1698
+
1699
+ try:
1700
+ # Get the event.
1701
+ event = Event(**{k: v for k, v in fields.items() if k in _EVENT_FIELDS})
1702
+ except (TypeError, ValueError) as ex:
1703
+ raise exceptions.EventDeserializationError(
1704
+ f"Failed to deserialize event data: {fields}."
1705
+ ) from ex
1577
1706
 
1578
1707
  self.token_to_sid[event.token] = sid
1579
1708
  self.sid_to_token[sid] = event.token
@@ -1602,7 +1731,7 @@ class EventNamespace(AsyncNamespace):
1602
1731
  # Emit the update from processing the event.
1603
1732
  await self.emit_update(update=update, sid=sid)
1604
1733
 
1605
- async def on_ping(self, sid):
1734
+ async def on_ping(self, sid: str):
1606
1735
  """Event for testing the API endpoint.
1607
1736
 
1608
1737
  Args: