reflex 0.6.8a2__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 (154) 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 -109
  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 -15
  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 +130 -161
  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 +29 -23
  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 -16
  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.8a2.dist-info → reflex-0.7.0a1.dist-info}/METADATA +7 -8
  150. {reflex-0.6.8a2.dist-info → reflex-0.7.0a1.dist-info}/RECORD +153 -149
  151. {reflex-0.6.8a2.dist-info → reflex-0.7.0a1.dist-info}/WHEEL +1 -1
  152. reflex/experimental/assets.py +0 -37
  153. {reflex-0.6.8a2.dist-info → reflex-0.7.0a1.dist-info}/LICENSE +0 -0
  154. {reflex-0.6.8a2.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
 
@@ -333,8 +362,8 @@ class App(MiddlewareMixin, LifespanMixin):
333
362
 
334
363
  def _enable_state(self) -> None:
335
364
  """Enable state for the app."""
336
- if not self.state:
337
- self.state = State
365
+ if not self._state:
366
+ self._state = State
338
367
  self._setup_state()
339
368
 
340
369
  def _setup_state(self) -> None:
@@ -343,13 +372,13 @@ class App(MiddlewareMixin, LifespanMixin):
343
372
  Raises:
344
373
  RuntimeError: If the socket server is invalid.
345
374
  """
346
- if not self.state:
375
+ if not self._state:
347
376
  return
348
377
 
349
378
  config = get_config()
350
379
 
351
380
  # Set up the state manager.
352
- self._state_manager = StateManager.create(state=self.state)
381
+ self._state_manager = StateManager.create(state=self._state)
353
382
 
354
383
  # Set up the Socket.IO AsyncServer.
355
384
  if not self.sio:
@@ -380,12 +409,42 @@ class App(MiddlewareMixin, LifespanMixin):
380
409
  namespace = config.get_event_namespace()
381
410
 
382
411
  # Create the event namespace and attach the main app. Not related to any paths.
383
- self.event_namespace = EventNamespace(namespace, self)
412
+ self._event_namespace = EventNamespace(namespace, self)
384
413
 
385
414
  # Register the event namespace with the socket.
386
415
  self.sio.register_namespace(self.event_namespace)
387
416
  # Mount the socket app with the API.
388
- 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)
389
448
 
390
449
  # Check the exception handlers
391
450
  self._validate_exception_handlers()
@@ -396,24 +455,35 @@ class App(MiddlewareMixin, LifespanMixin):
396
455
  Returns:
397
456
  The string representation of the app.
398
457
  """
399
- 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}>"
400
459
 
401
460
  def __call__(self) -> FastAPI:
402
461
  """Run the backend api instance.
403
462
 
463
+ Raises:
464
+ ValueError: If the app has not been initialized.
465
+
404
466
  Returns:
405
467
  The backend api.
406
468
  """
469
+ if not self.api:
470
+ raise ValueError("The app has not been initialized.")
407
471
  return self.api
408
472
 
409
473
  def _add_default_endpoints(self):
410
474
  """Add default api endpoints (ping)."""
411
475
  # To test the server.
476
+ if not self.api:
477
+ return
478
+
412
479
  self.api.get(str(constants.Endpoint.PING))(ping)
413
480
  self.api.get(str(constants.Endpoint.HEALTH))(health)
414
481
 
415
482
  def _add_optional_endpoints(self):
416
483
  """Add optional api endpoints (_upload)."""
484
+ if not self.api:
485
+ return
486
+
417
487
  if Upload.is_used:
418
488
  # To upload files.
419
489
  self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
@@ -431,6 +501,8 @@ class App(MiddlewareMixin, LifespanMixin):
431
501
 
432
502
  def _add_cors(self):
433
503
  """Add CORS middleware to the app."""
504
+ if not self.api:
505
+ return
434
506
  self.api.add_middleware(
435
507
  cors.CORSMiddleware,
436
508
  allow_credentials=True,
@@ -462,14 +534,8 @@ class App(MiddlewareMixin, LifespanMixin):
462
534
 
463
535
  Returns:
464
536
  The generated component.
465
-
466
- Raises:
467
- exceptions.MatchTypeError: If the return types of match cases in rx.match are different.
468
537
  """
469
- try:
470
- return component if isinstance(component, Component) else component()
471
- except exceptions.MatchTypeError:
472
- raise
538
+ return component if isinstance(component, Component) else component()
473
539
 
474
540
  def add_page(
475
541
  self,
@@ -526,13 +592,13 @@ class App(MiddlewareMixin, LifespanMixin):
526
592
  # Check if the route given is valid
527
593
  verify_route_validity(route)
528
594
 
529
- 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():
530
596
  # when the app is reloaded(typically for app harness tests), we should maintain
531
597
  # the latest render function of a route.This applies typically to decorated pages
532
598
  # since they are only added when app._compile is called.
533
- self.unevaluated_pages.pop(route)
599
+ self._unevaluated_pages.pop(route)
534
600
 
535
- if route in self.unevaluated_pages:
601
+ if route in self._unevaluated_pages:
536
602
  route_name = (
537
603
  f"`{route}` or `/`"
538
604
  if route == constants.PageNames.INDEX_ROUTE
@@ -545,15 +611,15 @@ class App(MiddlewareMixin, LifespanMixin):
545
611
 
546
612
  # Setup dynamic args for the route.
547
613
  # this state assignment is only required for tests using the deprecated state kwarg for App
548
- state = self.state if self.state else State
614
+ state = self._state if self._state else State
549
615
  state.setup_dynamic_args(get_route_args(route))
550
616
 
551
617
  if on_load:
552
- self.load_events[route] = (
618
+ self._load_events[route] = (
553
619
  on_load if isinstance(on_load, list) else [on_load]
554
620
  )
555
621
 
556
- self.unevaluated_pages[route] = UnevaluatedPage(
622
+ self._unevaluated_pages[route] = UnevaluatedPage(
557
623
  component=component,
558
624
  route=route,
559
625
  title=title,
@@ -563,14 +629,15 @@ class App(MiddlewareMixin, LifespanMixin):
563
629
  meta=meta,
564
630
  )
565
631
 
566
- def _compile_page(self, route: str):
632
+ def _compile_page(self, route: str, save_page: bool = True):
567
633
  """Compile a page.
568
634
 
569
635
  Args:
570
636
  route: The route of the page to compile.
637
+ save_page: If True, the compiled page is saved to self._pages.
571
638
  """
572
639
  component, enable_state = compiler.compile_unevaluated_page(
573
- route, self.unevaluated_pages[route], self.state, self.style, self.theme
640
+ route, self._unevaluated_pages[route], self._state, self.style, self.theme
574
641
  )
575
642
 
576
643
  if enable_state:
@@ -578,7 +645,8 @@ class App(MiddlewareMixin, LifespanMixin):
578
645
 
579
646
  # Add the page.
580
647
  self._check_routes_conflict(route)
581
- self.pages[route] = component
648
+ if save_page:
649
+ self._pages[route] = component
582
650
 
583
651
  def get_load_events(self, route: str) -> list[IndividualEventType[[], Any]]:
584
652
  """Get the load events for a route.
@@ -592,7 +660,7 @@ class App(MiddlewareMixin, LifespanMixin):
592
660
  route = route.lstrip("/")
593
661
  if route == "":
594
662
  route = constants.PageNames.INDEX_ROUTE
595
- return self.load_events.get(route, [])
663
+ return self._load_events.get(route, [])
596
664
 
597
665
  def _check_routes_conflict(self, new_route: str):
598
666
  """Verify if there is any conflict between the new route and any existing route.
@@ -616,10 +684,13 @@ class App(MiddlewareMixin, LifespanMixin):
616
684
  constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
617
685
  constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
618
686
  )
619
- for route in self.pages:
687
+ for route in self._pages:
620
688
  replaced_route = replace_brackets_with_keywords(route)
621
689
  for rw, r, nr in zip(
622
- replaced_route.split("/"), route.split("/"), new_route.split("/")
690
+ replaced_route.split("/"),
691
+ route.split("/"),
692
+ new_route.split("/"),
693
+ strict=False,
623
694
  ):
624
695
  if rw in segments and r != nr:
625
696
  # If the slugs in the segments of both routes are not the same, then the route is invalid
@@ -650,8 +721,8 @@ class App(MiddlewareMixin, LifespanMixin):
650
721
  Args:
651
722
  component: The component to display at the page.
652
723
  title: The title of the page.
653
- description: The description of the page.
654
724
  image: The image to display on the page.
725
+ description: The description of the page.
655
726
  on_load: The event handler(s) that will be called each time the page load.
656
727
  meta: The metadata of the page.
657
728
  """
@@ -674,6 +745,9 @@ class App(MiddlewareMixin, LifespanMixin):
674
745
  def _setup_admin_dash(self):
675
746
  """Setup the admin dash."""
676
747
  # Get the admin dash.
748
+ if not self.api:
749
+ return
750
+
677
751
  admin_dash = self.admin_dash
678
752
 
679
753
  if admin_dash and admin_dash.models:
@@ -715,7 +789,7 @@ class App(MiddlewareMixin, LifespanMixin):
715
789
  frontend_packages = get_config().frontend_packages
716
790
  _frontend_packages = []
717
791
  for package in frontend_packages:
718
- if package in (get_config().tailwind or {}).get("plugins", []): # type: ignore
792
+ if package in (get_config().tailwind or {}).get("plugins", []):
719
793
  console.warn(
720
794
  f"Tailwind packages are inferred from 'plugins', remove `{package}` from `frontend_packages`"
721
795
  )
@@ -778,10 +852,10 @@ class App(MiddlewareMixin, LifespanMixin):
778
852
 
779
853
  def _setup_overlay_component(self):
780
854
  """If a State is not used and no overlay_component is specified, do not render the connection modal."""
781
- 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:
782
856
  self.overlay_component = None
783
- for k, component in self.pages.items():
784
- 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)
785
859
 
786
860
  def _add_error_boundary_to_component(self, component: Component) -> Component:
787
861
  if self.error_boundary is None:
@@ -793,14 +867,23 @@ class App(MiddlewareMixin, LifespanMixin):
793
867
 
794
868
  def _setup_error_boundary(self):
795
869
  """If a State is not used and no error_boundary is specified, do not render the error boundary."""
796
- 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:
797
871
  self.error_boundary = None
798
872
 
799
- for k, component in self.pages.items():
873
+ for k, component in self._pages.items():
800
874
  # Skip the 404 page
801
875
  if k == constants.Page404.SLUG:
802
876
  continue
803
- 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)
804
887
 
805
888
  def _apply_decorated_pages(self):
806
889
  """Add @rx.page decorated pages to the app.
@@ -826,21 +909,27 @@ class App(MiddlewareMixin, LifespanMixin):
826
909
  Raises:
827
910
  VarDependencyError: When a computed var has an invalid dependency.
828
911
  """
829
- if not self.state:
912
+ if not self._state:
830
913
  return
831
914
 
832
915
  if not state:
833
- state = self.state
916
+ state = self._state
834
917
 
835
918
  for var in state.computed_vars.values():
836
919
  if not var._cache:
837
920
  continue
838
921
  deps = var._deps(objclass=state)
839
- for dep in deps:
840
- if dep not in state.vars and dep not in state.backend_vars:
841
- raise exceptions.VarDependencyError(
842
- f"ComputedVar {var._js_expr} on state {state.__name__} has an invalid dependency {dep}"
843
- )
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
+ )
844
933
 
845
934
  for substate in state.class_subclasses:
846
935
  self._validate_var_dependencies(substate)
@@ -856,13 +945,13 @@ class App(MiddlewareMixin, LifespanMixin):
856
945
  """
857
946
  from reflex.utils.exceptions import ReflexRuntimeError
858
947
 
859
- self.pages = {}
948
+ self._pages = {}
860
949
 
861
950
  def get_compilation_time() -> str:
862
951
  return str(datetime.now().time()).split(".")[0]
863
952
 
864
953
  # Render a default 404 page if the user didn't supply one
865
- if constants.Page404.SLUG not in self.unevaluated_pages:
954
+ if constants.Page404.SLUG not in self._unevaluated_pages:
866
955
  self.add_page(route=constants.Page404.SLUG)
867
956
 
868
957
  # Fix up the style.
@@ -878,19 +967,23 @@ class App(MiddlewareMixin, LifespanMixin):
878
967
  # If a theme component was provided, wrap the app with it
879
968
  app_wrappers[(20, "Theme")] = self.theme
880
969
 
881
- for route in self.unevaluated_pages:
882
- console.debug(f"Evaluating page: {route}")
883
- self._compile_page(route)
970
+ # Get the env mode.
971
+ config = get_config()
884
972
 
885
- # Add the optional endpoints (_upload)
886
- self._add_optional_endpoints()
973
+ if config.react_strict_mode:
974
+ app_wrappers[(200, "StrictMode")] = StrictMode.create()
887
975
 
888
- if not self._should_compile():
889
- return
976
+ should_compile = self._should_compile()
890
977
 
891
- self._validate_var_dependencies()
892
- self._setup_overlay_component()
893
- 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
894
987
 
895
988
  # Create a progress bar.
896
989
  progress = Progress(
@@ -900,18 +993,32 @@ class App(MiddlewareMixin, LifespanMixin):
900
993
  )
901
994
 
902
995
  # try to be somewhat accurate - but still not 100%
903
- adhoc_steps_without_executor = 6
996
+ adhoc_steps_without_executor = 7
904
997
  fixed_pages_within_executor = 5
905
998
  progress.start()
906
999
  task = progress.add_task(
907
1000
  f"[{get_compilation_time()}] Compiling:",
908
- total=len(self.pages)
1001
+ total=len(self._pages)
1002
+ + (len(self._unevaluated_pages) * 2)
909
1003
  + fixed_pages_within_executor
910
1004
  + adhoc_steps_without_executor,
911
1005
  )
912
1006
 
913
- # Get the env mode.
914
- 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)
915
1022
 
916
1023
  # Store the compile results.
917
1024
  compile_results = []
@@ -924,7 +1031,7 @@ class App(MiddlewareMixin, LifespanMixin):
924
1031
 
925
1032
  # This has to happen before compiling stateful components as that
926
1033
  # prevents recursive functions from reaching all components.
927
- for component in self.pages.values():
1034
+ for component in self._pages.values():
928
1035
  # Add component._get_all_imports() to all_imports.
929
1036
  all_imports.update(component._get_all_imports())
930
1037
 
@@ -939,12 +1046,12 @@ class App(MiddlewareMixin, LifespanMixin):
939
1046
  stateful_components_path,
940
1047
  stateful_components_code,
941
1048
  page_components,
942
- ) = compiler.compile_stateful_components(self.pages.values())
1049
+ ) = compiler.compile_stateful_components(self._pages.values())
943
1050
 
944
1051
  progress.advance(task)
945
1052
 
946
1053
  # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
947
- 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:
948
1055
  raise ReflexRuntimeError(
949
1056
  "To access rx.State in frontend components, at least one "
950
1057
  "subclass of rx.State must be defined in the app."
@@ -958,7 +1065,7 @@ class App(MiddlewareMixin, LifespanMixin):
958
1065
  compiler.compile_document_root(
959
1066
  self.head_components,
960
1067
  html_lang=self.html_lang,
961
- html_custom_attrs=self.html_custom_attrs, # type: ignore
1068
+ html_custom_attrs=self.html_custom_attrs, # pyright: ignore [reportArgumentType]
962
1069
  )
963
1070
  )
964
1071
 
@@ -981,20 +1088,20 @@ class App(MiddlewareMixin, LifespanMixin):
981
1088
  max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
982
1089
  )
983
1090
 
984
- for route, component in zip(self.pages, page_components):
1091
+ for route, component in zip(self._pages, page_components, strict=True):
985
1092
  ExecutorSafeFunctions.COMPONENTS[route] = component
986
1093
 
987
- ExecutorSafeFunctions.STATE = self.state
1094
+ ExecutorSafeFunctions.STATE = self._state
988
1095
 
989
1096
  with executor:
990
1097
  result_futures = []
991
1098
 
992
- def _submit_work(fn, *args, **kwargs):
1099
+ def _submit_work(fn: Callable, *args, **kwargs):
993
1100
  f = executor.submit(fn, *args, **kwargs)
994
1101
  result_futures.append(f)
995
1102
 
996
1103
  # Compile the pre-compiled pages.
997
- for route in self.pages:
1104
+ for route in self._pages:
998
1105
  _submit_work(
999
1106
  ExecutorSafeFunctions.compile_page,
1000
1107
  route,
@@ -1029,7 +1136,7 @@ class App(MiddlewareMixin, LifespanMixin):
1029
1136
 
1030
1137
  # Compile the contexts.
1031
1138
  compile_results.append(
1032
- compiler.compile_contexts(self.state, self.theme),
1139
+ compiler.compile_contexts(self._state, self.theme),
1033
1140
  )
1034
1141
  if self.theme is not None:
1035
1142
  # Fix #2992 by removing the top-level appearance prop
@@ -1151,9 +1258,9 @@ class App(MiddlewareMixin, LifespanMixin):
1151
1258
  )
1152
1259
 
1153
1260
  task = asyncio.create_task(_coro())
1154
- self.background_tasks.add(task)
1261
+ self._background_tasks.add(task)
1155
1262
  # Clean up task from background_tasks set when complete.
1156
- task.add_done_callback(self.background_tasks.discard)
1263
+ task.add_done_callback(self._background_tasks.discard)
1157
1264
  return task
1158
1265
 
1159
1266
  def _validate_exception_handlers(self):
@@ -1163,11 +1270,11 @@ class App(MiddlewareMixin, LifespanMixin):
1163
1270
  ValueError: If the custom exception handlers are invalid.
1164
1271
 
1165
1272
  """
1166
- FRONTEND_ARG_SPEC = {
1273
+ frontend_arg_spec = {
1167
1274
  "exception": Exception,
1168
1275
  }
1169
1276
 
1170
- BACKEND_ARG_SPEC = {
1277
+ backend_arg_spec = {
1171
1278
  "exception": Exception,
1172
1279
  }
1173
1280
 
@@ -1175,9 +1282,10 @@ class App(MiddlewareMixin, LifespanMixin):
1175
1282
  ["frontend", "backend"],
1176
1283
  [self.frontend_exception_handler, self.backend_exception_handler],
1177
1284
  [
1178
- FRONTEND_ARG_SPEC,
1179
- BACKEND_ARG_SPEC,
1285
+ frontend_arg_spec,
1286
+ backend_arg_spec,
1180
1287
  ],
1288
+ strict=True,
1181
1289
  ):
1182
1290
  if hasattr(handler_fn, "__name__"):
1183
1291
  _fn_name = handler_fn.__name__
@@ -1218,7 +1326,7 @@ class App(MiddlewareMixin, LifespanMixin):
1218
1326
  ):
1219
1327
  raise ValueError(
1220
1328
  f"Provided custom {handler_domain} exception handler `{_fn_name}` has the wrong argument order."
1221
- 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]}`"
1222
1330
  )
1223
1331
 
1224
1332
  if not issubclass(arg_annotations[required_arg], Exception):
@@ -1319,15 +1427,14 @@ async def process(
1319
1427
  if app._process_background(state, event) is not None:
1320
1428
  # `final=True` allows the frontend send more events immediately.
1321
1429
  yield StateUpdate(final=True)
1322
- return
1323
-
1324
- # Process the event synchronously.
1325
- async for update in state._process(event):
1326
- # Postprocess the event.
1327
- update = await app._postprocess(state, event, update)
1328
-
1329
- # Yield the update.
1330
- 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
1331
1438
  except Exception as ex:
1332
1439
  telemetry.send_error(ex, context="backend")
1333
1440
 
@@ -1522,16 +1629,20 @@ class EventNamespace(AsyncNamespace):
1522
1629
  self.sid_to_token = {}
1523
1630
  self.app = app
1524
1631
 
1525
- def on_connect(self, sid, environ):
1632
+ def on_connect(self, sid: str, environ: dict):
1526
1633
  """Event for when the websocket is connected.
1527
1634
 
1528
1635
  Args:
1529
1636
  sid: The Socket.IO session id.
1530
1637
  environ: The request information, including HTTP headers.
1531
1638
  """
1532
- 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
+ )
1533
1644
 
1534
- def on_disconnect(self, sid):
1645
+ def on_disconnect(self, sid: str):
1535
1646
  """Event for when the websocket disconnects.
1536
1647
 
1537
1648
  Args:
@@ -1553,7 +1664,7 @@ class EventNamespace(AsyncNamespace):
1553
1664
  self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
1554
1665
  )
1555
1666
 
1556
- async def on_event(self, sid, data):
1667
+ async def on_event(self, sid: str, data: Any):
1557
1668
  """Event for receiving front-end websocket events.
1558
1669
 
1559
1670
  Raises:
@@ -1562,12 +1673,36 @@ class EventNamespace(AsyncNamespace):
1562
1673
  Args:
1563
1674
  sid: The Socket.IO session id.
1564
1675
  data: The event data.
1676
+
1677
+ Raises:
1678
+ EventDeserializationError: If the event data is not a dictionary.
1565
1679
  """
1566
1680
  fields = data
1567
- # Get the event.
1568
- event = Event(
1569
- **{k: v for k, v in fields.items() if k not in ("handler", "event_actions")}
1570
- )
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
1571
1706
 
1572
1707
  self.token_to_sid[event.token] = sid
1573
1708
  self.sid_to_token[sid] = event.token
@@ -1596,7 +1731,7 @@ class EventNamespace(AsyncNamespace):
1596
1731
  # Emit the update from processing the event.
1597
1732
  await self.emit_update(update=update, sid=sid)
1598
1733
 
1599
- async def on_ping(self, sid):
1734
+ async def on_ping(self, sid: str):
1600
1735
  """Event for testing the API endpoint.
1601
1736
 
1602
1737
  Args: