reflex 0.6.8a2__py3-none-any.whl → 0.7.0__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 (247) 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 +3 -3
  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 +286 -129
  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 +57 -18
  15. reflex/components/base/app_wrap.pyi +16 -16
  16. reflex/components/base/bare.py +1 -1
  17. reflex/components/base/body.pyi +16 -16
  18. reflex/components/base/document.pyi +76 -76
  19. reflex/components/base/error_boundary.py +2 -1
  20. reflex/components/base/error_boundary.pyi +19 -22
  21. reflex/components/base/fragment.pyi +16 -16
  22. reflex/components/base/head.pyi +31 -31
  23. reflex/components/base/link.pyi +31 -31
  24. reflex/components/base/meta.py +2 -2
  25. reflex/components/base/meta.pyi +61 -61
  26. reflex/components/base/script.pyi +19 -19
  27. reflex/components/base/strict_mode.py +10 -0
  28. reflex/components/base/strict_mode.pyi +57 -0
  29. reflex/components/component.py +38 -77
  30. reflex/components/core/banner.py +159 -4
  31. reflex/components/core/banner.pyi +162 -76
  32. reflex/components/core/breakpoints.py +3 -1
  33. reflex/components/core/client_side_routing.py +1 -1
  34. reflex/components/core/client_side_routing.pyi +32 -32
  35. reflex/components/core/clipboard.pyi +17 -20
  36. reflex/components/core/cond.py +9 -10
  37. reflex/components/core/debounce.py +1 -1
  38. reflex/components/core/debounce.pyi +17 -17
  39. reflex/components/core/foreach.py +28 -3
  40. reflex/components/core/html.py +1 -1
  41. reflex/components/core/html.pyi +16 -16
  42. reflex/components/core/match.py +5 -5
  43. reflex/components/core/sticky.py +134 -0
  44. reflex/components/core/sticky.pyi +449 -0
  45. reflex/components/core/upload.py +2 -2
  46. reflex/components/core/upload.pyi +80 -88
  47. reflex/components/datadisplay/code.py +5 -14
  48. reflex/components/datadisplay/code.pyi +31 -31
  49. reflex/components/datadisplay/dataeditor.py +7 -4
  50. reflex/components/datadisplay/dataeditor.pyi +40 -54
  51. reflex/components/datadisplay/logo.py +13 -8
  52. reflex/components/datadisplay/shiki_code_block.py +14 -9
  53. reflex/components/datadisplay/shiki_code_block.pyi +46 -46
  54. reflex/components/dynamic.py +22 -3
  55. reflex/components/el/constants/reflex.py +1 -1
  56. reflex/components/el/element.py +1 -1
  57. reflex/components/el/element.pyi +16 -16
  58. reflex/components/el/elements/base.pyi +16 -16
  59. reflex/components/el/elements/forms.py +4 -4
  60. reflex/components/el/elements/forms.pyi +224 -258
  61. reflex/components/el/elements/inline.pyi +421 -421
  62. reflex/components/el/elements/media.pyi +376 -376
  63. reflex/components/el/elements/metadata.pyi +91 -91
  64. reflex/components/el/elements/other.pyi +106 -106
  65. reflex/components/el/elements/scripts.pyi +46 -46
  66. reflex/components/el/elements/sectioning.pyi +226 -226
  67. reflex/components/el/elements/tables.pyi +151 -151
  68. reflex/components/el/elements/typography.pyi +226 -226
  69. reflex/components/gridjs/datatable.pyi +31 -31
  70. reflex/components/lucide/icon.py +46 -8
  71. reflex/components/lucide/icon.pyi +85 -31
  72. reflex/components/markdown/markdown.py +10 -8
  73. reflex/components/markdown/markdown.pyi +16 -16
  74. reflex/components/moment/moment.py +2 -2
  75. reflex/components/moment/moment.pyi +17 -19
  76. reflex/components/next/base.pyi +16 -16
  77. reflex/components/next/image.py +16 -4
  78. reflex/components/next/image.pyi +22 -20
  79. reflex/components/next/link.py +1 -1
  80. reflex/components/next/link.pyi +16 -16
  81. reflex/components/next/video.pyi +16 -16
  82. reflex/components/plotly/__init__.py +29 -2
  83. reflex/components/plotly/plotly.py +240 -5
  84. reflex/components/plotly/plotly.pyi +799 -44
  85. reflex/components/props.py +3 -3
  86. reflex/components/radix/__init__.pyi +1 -1
  87. reflex/components/radix/primitives/accordion.py +9 -5
  88. reflex/components/radix/primitives/accordion.pyi +110 -108
  89. reflex/components/radix/primitives/base.pyi +31 -31
  90. reflex/components/radix/primitives/drawer.py +5 -2
  91. reflex/components/radix/primitives/drawer.pyi +179 -187
  92. reflex/components/radix/primitives/form.pyi +160 -172
  93. reflex/components/radix/primitives/progress.py +1 -1
  94. reflex/components/radix/primitives/progress.pyi +76 -76
  95. reflex/components/radix/primitives/slider.py +1 -1
  96. reflex/components/radix/primitives/slider.pyi +78 -82
  97. reflex/components/radix/themes/base.pyi +121 -121
  98. reflex/components/radix/themes/color_mode.py +11 -9
  99. reflex/components/radix/themes/color_mode.pyi +47 -49
  100. reflex/components/radix/themes/components/alert_dialog.py +3 -0
  101. reflex/components/radix/themes/components/alert_dialog.pyi +110 -112
  102. reflex/components/radix/themes/components/aspect_ratio.pyi +16 -16
  103. reflex/components/radix/themes/components/avatar.pyi +16 -16
  104. reflex/components/radix/themes/components/badge.pyi +16 -16
  105. reflex/components/radix/themes/components/button.pyi +16 -16
  106. reflex/components/radix/themes/components/callout.pyi +76 -76
  107. reflex/components/radix/themes/components/card.py +1 -1
  108. reflex/components/radix/themes/components/card.pyi +17 -17
  109. reflex/components/radix/themes/components/checkbox.pyi +49 -55
  110. reflex/components/radix/themes/components/checkbox_cards.pyi +31 -31
  111. reflex/components/radix/themes/components/checkbox_group.pyi +31 -31
  112. reflex/components/radix/themes/components/context_menu.py +5 -0
  113. reflex/components/radix/themes/components/context_menu.pyi +149 -155
  114. reflex/components/radix/themes/components/data_list.pyi +61 -61
  115. reflex/components/radix/themes/components/dialog.py +3 -0
  116. reflex/components/radix/themes/components/dialog.pyi +113 -117
  117. reflex/components/radix/themes/components/dropdown_menu.py +5 -0
  118. reflex/components/radix/themes/components/dropdown_menu.pyi +133 -137
  119. reflex/components/radix/themes/components/hover_card.py +3 -0
  120. reflex/components/radix/themes/components/hover_card.pyi +63 -67
  121. reflex/components/radix/themes/components/icon_button.py +2 -2
  122. reflex/components/radix/themes/components/icon_button.pyi +17 -16
  123. reflex/components/radix/themes/components/inset.pyi +16 -16
  124. reflex/components/radix/themes/components/popover.py +3 -0
  125. reflex/components/radix/themes/components/popover.pyi +68 -70
  126. reflex/components/radix/themes/components/progress.pyi +16 -16
  127. reflex/components/radix/themes/components/radio.pyi +16 -16
  128. reflex/components/radix/themes/components/radio_cards.py +2 -0
  129. reflex/components/radix/themes/components/radio_cards.pyi +32 -34
  130. reflex/components/radix/themes/components/radio_group.py +1 -1
  131. reflex/components/radix/themes/components/radio_group.pyi +62 -64
  132. reflex/components/radix/themes/components/scroll_area.pyi +16 -16
  133. reflex/components/radix/themes/components/segmented_control.pyi +32 -35
  134. reflex/components/radix/themes/components/select.py +4 -0
  135. reflex/components/radix/themes/components/select.pyi +145 -157
  136. reflex/components/radix/themes/components/separator.pyi +16 -16
  137. reflex/components/radix/themes/components/skeleton.py +3 -0
  138. reflex/components/radix/themes/components/skeleton.pyi +16 -16
  139. reflex/components/radix/themes/components/slider.pyi +22 -28
  140. reflex/components/radix/themes/components/spinner.pyi +16 -16
  141. reflex/components/radix/themes/components/switch.pyi +17 -19
  142. reflex/components/radix/themes/components/table.pyi +106 -106
  143. reflex/components/radix/themes/components/tabs.py +3 -0
  144. reflex/components/radix/themes/components/tabs.pyi +78 -82
  145. reflex/components/radix/themes/components/text_area.py +12 -0
  146. reflex/components/radix/themes/components/text_area.pyi +21 -33
  147. reflex/components/radix/themes/components/text_field.py +1 -1
  148. reflex/components/radix/themes/components/text_field.pyi +52 -80
  149. reflex/components/radix/themes/components/tooltip.py +6 -1
  150. reflex/components/radix/themes/components/tooltip.pyi +20 -21
  151. reflex/components/radix/themes/layout/__init__.pyi +1 -1
  152. reflex/components/radix/themes/layout/base.pyi +16 -16
  153. reflex/components/radix/themes/layout/box.pyi +16 -16
  154. reflex/components/radix/themes/layout/center.pyi +16 -16
  155. reflex/components/radix/themes/layout/container.pyi +16 -16
  156. reflex/components/radix/themes/layout/flex.pyi +16 -16
  157. reflex/components/radix/themes/layout/grid.pyi +16 -16
  158. reflex/components/radix/themes/layout/list.py +2 -2
  159. reflex/components/radix/themes/layout/list.pyi +76 -76
  160. reflex/components/radix/themes/layout/section.pyi +16 -16
  161. reflex/components/radix/themes/layout/spacer.pyi +16 -16
  162. reflex/components/radix/themes/layout/stack.py +2 -2
  163. reflex/components/radix/themes/layout/stack.pyi +46 -46
  164. reflex/components/radix/themes/typography/blockquote.pyi +16 -16
  165. reflex/components/radix/themes/typography/code.pyi +16 -16
  166. reflex/components/radix/themes/typography/heading.pyi +16 -16
  167. reflex/components/radix/themes/typography/link.py +1 -1
  168. reflex/components/radix/themes/typography/link.pyi +16 -16
  169. reflex/components/radix/themes/typography/text.py +2 -2
  170. reflex/components/radix/themes/typography/text.pyi +106 -106
  171. reflex/components/react_player/audio.pyi +33 -39
  172. reflex/components/react_player/react_player.py +1 -1
  173. reflex/components/react_player/react_player.pyi +32 -38
  174. reflex/components/react_player/video.pyi +33 -39
  175. reflex/components/recharts/__init__.py +2 -0
  176. reflex/components/recharts/__init__.pyi +2 -0
  177. reflex/components/recharts/cartesian.pyi +282 -282
  178. reflex/components/recharts/charts.py +15 -15
  179. reflex/components/recharts/charts.pyi +164 -164
  180. reflex/components/recharts/general.py +19 -4
  181. reflex/components/recharts/general.pyi +132 -81
  182. reflex/components/recharts/polar.py +2 -2
  183. reflex/components/recharts/polar.pyi +55 -55
  184. reflex/components/recharts/recharts.py +4 -4
  185. reflex/components/recharts/recharts.pyi +31 -31
  186. reflex/components/sonner/toast.py +15 -13
  187. reflex/components/sonner/toast.pyi +22 -22
  188. reflex/components/suneditor/editor.py +6 -4
  189. reflex/components/suneditor/editor.pyi +26 -40
  190. reflex/components/tags/iter_tag.py +3 -3
  191. reflex/components/tags/tag.py +25 -3
  192. reflex/config.py +48 -15
  193. reflex/constants/__init__.py +1 -0
  194. reflex/constants/base.py +4 -1
  195. reflex/constants/compiler.py +5 -2
  196. reflex/constants/config.py +8 -1
  197. reflex/constants/installer.py +9 -9
  198. reflex/constants/style.py +1 -1
  199. reflex/custom_components/custom_components.py +18 -10
  200. reflex/event.py +221 -231
  201. reflex/experimental/__init__.py +19 -11
  202. reflex/experimental/client_state.py +53 -28
  203. reflex/experimental/hooks.py +5 -5
  204. reflex/experimental/layout.py +8 -5
  205. reflex/experimental/layout.pyi +79 -83
  206. reflex/experimental/misc.py +3 -3
  207. reflex/istate/wrappers.py +1 -1
  208. reflex/middleware/hydrate_middleware.py +2 -2
  209. reflex/model.py +11 -6
  210. reflex/page.py +5 -5
  211. reflex/reflex.py +104 -26
  212. reflex/route.py +1 -1
  213. reflex/state.py +358 -401
  214. reflex/style.py +27 -3
  215. reflex/testing.py +29 -23
  216. reflex/utils/build.py +6 -2
  217. reflex/utils/codespaces.py +1 -4
  218. reflex/utils/compat.py +6 -5
  219. reflex/utils/console.py +71 -16
  220. reflex/utils/exceptions.py +89 -26
  221. reflex/utils/exec.py +69 -74
  222. reflex/utils/export.py +6 -1
  223. reflex/utils/format.py +8 -40
  224. reflex/utils/imports.py +5 -2
  225. reflex/utils/lazy_loader.py +7 -1
  226. reflex/utils/path_ops.py +74 -14
  227. reflex/utils/prerequisites.py +345 -68
  228. reflex/utils/processes.py +45 -32
  229. reflex/utils/pyi_generator.py +39 -33
  230. reflex/utils/registry.py +4 -4
  231. reflex/utils/serializers.py +1 -1
  232. reflex/utils/telemetry.py +5 -4
  233. reflex/utils/types.py +42 -18
  234. reflex/vars/base.py +695 -330
  235. reflex/vars/datetime.py +6 -7
  236. reflex/vars/dep_tracking.py +344 -0
  237. reflex/vars/function.py +11 -5
  238. reflex/vars/number.py +31 -43
  239. reflex/vars/object.py +74 -64
  240. reflex/vars/sequence.py +79 -67
  241. {reflex-0.6.8a2.dist-info → reflex-0.7.0.dist-info}/METADATA +7 -8
  242. reflex-0.7.0.dist-info/RECORD +401 -0
  243. {reflex-0.6.8a2.dist-info → reflex-0.7.0.dist-info}/WHEEL +1 -1
  244. reflex/experimental/assets.py +0 -37
  245. reflex-0.6.8a2.dist-info/RECORD +0 -397
  246. {reflex-0.6.8a2.dist-info → reflex-0.7.0.dist-info}/LICENSE +0 -0
  247. {reflex-0.6.8a2.dist-info → reflex-0.7.0.dist-info}/entry_points.txt +0 -0
reflex/app.py CHANGED
@@ -25,8 +25,8 @@ from typing import (
25
25
  Callable,
26
26
  Coroutine,
27
27
  Dict,
28
- Generic,
29
28
  List,
29
+ MutableMapping,
30
30
  Optional,
31
31
  Set,
32
32
  Type,
@@ -53,22 +53,28 @@ from reflex.compiler.compiler import ExecutorSafeFunctions, compile_theme
53
53
  from reflex.components.base.app_wrap import AppWrap
54
54
  from reflex.components.base.error_boundary import ErrorBoundary
55
55
  from reflex.components.base.fragment import Fragment
56
+ from reflex.components.base.strict_mode import StrictMode
56
57
  from reflex.components.component import (
57
58
  Component,
58
59
  ComponentStyle,
59
60
  evaluate_style_namespaces,
60
61
  )
61
- from reflex.components.core.banner import connection_pulser, connection_toaster
62
+ from reflex.components.core.banner import (
63
+ backend_disabled,
64
+ connection_pulser,
65
+ connection_toaster,
66
+ )
62
67
  from reflex.components.core.breakpoints import set_breakpoints
63
68
  from reflex.components.core.client_side_routing import (
64
69
  Default404Page,
65
70
  wait_for_client_redirect,
66
71
  )
72
+ from reflex.components.core.sticky import sticky
67
73
  from reflex.components.core.upload import Upload, get_upload_dir
68
74
  from reflex.components.radix import themes
69
75
  from reflex.config import environment, get_config
70
76
  from reflex.event import (
71
- BASE_STATE,
77
+ _EVENT_FIELDS,
72
78
  Event,
73
79
  EventHandler,
74
80
  EventSpec,
@@ -93,7 +99,15 @@ from reflex.state import (
93
99
  _substate_key,
94
100
  code_uses_state_contexts,
95
101
  )
96
- from reflex.utils import codespaces, console, exceptions, format, prerequisites, types
102
+ from reflex.utils import (
103
+ codespaces,
104
+ console,
105
+ exceptions,
106
+ format,
107
+ path_ops,
108
+ prerequisites,
109
+ types,
110
+ )
97
111
  from reflex.utils.exec import is_prod_mode, is_testing_env
98
112
  from reflex.utils.imports import ImportVar
99
113
 
@@ -144,7 +158,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
144
158
  position="top-center",
145
159
  id="backend_error",
146
160
  style={"width": "500px"},
147
- ) # type: ignore
161
+ )
148
162
  else:
149
163
  error_message.insert(0, "An error occurred.")
150
164
  return window_alert("\n".join(error_message))
@@ -156,9 +170,12 @@ def default_overlay_component() -> Component:
156
170
  Returns:
157
171
  The default overlay_component, which is a connection_modal.
158
172
  """
173
+ config = get_config()
174
+
159
175
  return Fragment.create(
160
176
  connection_pulser(),
161
177
  connection_toaster(),
178
+ *([backend_disabled()] if config.is_reflex_cloud else []),
162
179
  *codespaces.codespaces_auto_redirect(),
163
180
  )
164
181
 
@@ -185,7 +202,7 @@ class OverlayFragment(Fragment):
185
202
  @dataclasses.dataclass(
186
203
  frozen=True,
187
204
  )
188
- class UnevaluatedPage(Generic[BASE_STATE]):
205
+ class UnevaluatedPage:
189
206
  """An uncompiled page."""
190
207
 
191
208
  component: Union[Component, ComponentCallable]
@@ -193,7 +210,7 @@ class UnevaluatedPage(Generic[BASE_STATE]):
193
210
  title: Union[Var, str, None]
194
211
  description: Union[Var, str, None]
195
212
  image: str
196
- on_load: Union[EventType[[], BASE_STATE], None]
213
+ on_load: Union[EventType[()], None]
197
214
  meta: List[Dict[str, str]]
198
215
 
199
216
 
@@ -250,36 +267,36 @@ class App(MiddlewareMixin, LifespanMixin):
250
267
  # Attributes to add to the html root tag of every page.
251
268
  html_custom_attrs: Optional[Dict[str, str]] = None
252
269
 
253
- # A map from a route to an unevaluated page. PRIVATE.
254
- unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
270
+ # A map from a route to an unevaluated page.
271
+ _unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
255
272
  default_factory=dict
256
273
  )
257
274
 
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)
275
+ # A map from a page route to the component to render. Users should use `add_page`.
276
+ _pages: Dict[str, Component] = dataclasses.field(default_factory=dict)
260
277
 
261
- # The backend API object. PRIVATE.
262
- api: FastAPI = None # type: ignore
278
+ # The backend API object.
279
+ _api: FastAPI | None = None
263
280
 
264
- # The state class to use for the app. PRIVATE.
265
- state: Optional[Type[BaseState]] = None
281
+ # The state class to use for the app.
282
+ _state: Optional[Type[BaseState]] = None
266
283
 
267
284
  # Class to manage many client states.
268
285
  _state_manager: Optional[StateManager] = None
269
286
 
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(
287
+ # Mapping from a route to event handlers to trigger when the page loads.
288
+ _load_events: Dict[str, List[IndividualEventType[()]]] = dataclasses.field(
272
289
  default_factory=dict
273
290
  )
274
291
 
275
- # Admin dashboard to view and manage the database. PRIVATE.
292
+ # Admin dashboard to view and manage the database.
276
293
  admin_dash: Optional[AdminDash] = None
277
294
 
278
- # The async server name space. PRIVATE.
279
- event_namespace: Optional[EventNamespace] = None
295
+ # The async server name space.
296
+ _event_namespace: Optional[EventNamespace] = None
280
297
 
281
- # Background tasks that are currently running. PRIVATE.
282
- background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
298
+ # Background tasks that are currently running.
299
+ _background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
283
300
 
284
301
  # Frontend Error Handler Function
285
302
  frontend_exception_handler: Callable[[Exception], None] = (
@@ -291,6 +308,24 @@ class App(MiddlewareMixin, LifespanMixin):
291
308
  [Exception], Union[EventSpec, List[EventSpec], None]
292
309
  ] = default_backend_exception_handler
293
310
 
311
+ @property
312
+ def api(self) -> FastAPI | None:
313
+ """Get the backend api.
314
+
315
+ Returns:
316
+ The backend api.
317
+ """
318
+ return self._api
319
+
320
+ @property
321
+ def event_namespace(self) -> EventNamespace | None:
322
+ """Get the event namespace.
323
+
324
+ Returns:
325
+ The event namespace.
326
+ """
327
+ return self._event_namespace
328
+
294
329
  def __post_init__(self):
295
330
  """Initialize the app.
296
331
 
@@ -310,7 +345,7 @@ class App(MiddlewareMixin, LifespanMixin):
310
345
  set_breakpoints(self.style.pop("breakpoints"))
311
346
 
312
347
  # Set up the API.
313
- self.api = FastAPI(lifespan=self._run_lifespan_tasks)
348
+ self._api = FastAPI(lifespan=self._run_lifespan_tasks)
314
349
  self._add_cors()
315
350
  self._add_default_endpoints()
316
351
 
@@ -333,8 +368,8 @@ class App(MiddlewareMixin, LifespanMixin):
333
368
 
334
369
  def _enable_state(self) -> None:
335
370
  """Enable state for the app."""
336
- if not self.state:
337
- self.state = State
371
+ if not self._state:
372
+ self._state = State
338
373
  self._setup_state()
339
374
 
340
375
  def _setup_state(self) -> None:
@@ -343,13 +378,13 @@ class App(MiddlewareMixin, LifespanMixin):
343
378
  Raises:
344
379
  RuntimeError: If the socket server is invalid.
345
380
  """
346
- if not self.state:
381
+ if not self._state:
347
382
  return
348
383
 
349
384
  config = get_config()
350
385
 
351
386
  # Set up the state manager.
352
- self._state_manager = StateManager.create(state=self.state)
387
+ self._state_manager = StateManager.create(state=self._state)
353
388
 
354
389
  # Set up the Socket.IO AsyncServer.
355
390
  if not self.sio:
@@ -380,12 +415,42 @@ class App(MiddlewareMixin, LifespanMixin):
380
415
  namespace = config.get_event_namespace()
381
416
 
382
417
  # Create the event namespace and attach the main app. Not related to any paths.
383
- self.event_namespace = EventNamespace(namespace, self)
418
+ self._event_namespace = EventNamespace(namespace, self)
384
419
 
385
420
  # Register the event namespace with the socket.
386
421
  self.sio.register_namespace(self.event_namespace)
387
422
  # Mount the socket app with the API.
388
- self.api.mount(str(constants.Endpoint.EVENT), socket_app)
423
+ if self.api:
424
+
425
+ class HeaderMiddleware:
426
+ def __init__(self, app: ASGIApp):
427
+ self.app = app
428
+
429
+ async def __call__(
430
+ self, scope: MutableMapping[str, Any], receive: Any, send: Callable
431
+ ):
432
+ original_send = send
433
+
434
+ async def modified_send(message: dict):
435
+ if message["type"] == "websocket.accept":
436
+ if scope.get("subprotocols"):
437
+ # The following *does* say "subprotocol" instead of "subprotocols", intentionally.
438
+ message["subprotocol"] = scope["subprotocols"][0]
439
+
440
+ headers = dict(message.get("headers", []))
441
+ header_key = b"sec-websocket-protocol"
442
+ if subprotocol := headers.get(header_key):
443
+ message["headers"] = [
444
+ *message.get("headers", []),
445
+ (header_key, subprotocol),
446
+ ]
447
+
448
+ return await original_send(message)
449
+
450
+ return await self.app(scope, receive, modified_send)
451
+
452
+ socket_app_with_headers = HeaderMiddleware(socket_app)
453
+ self.api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers)
389
454
 
390
455
  # Check the exception handlers
391
456
  self._validate_exception_handlers()
@@ -396,24 +461,35 @@ class App(MiddlewareMixin, LifespanMixin):
396
461
  Returns:
397
462
  The string representation of the app.
398
463
  """
399
- return f"<App state={self.state.__name__ if self.state else None}>"
464
+ return f"<App state={self._state.__name__ if self._state else None}>"
400
465
 
401
466
  def __call__(self) -> FastAPI:
402
467
  """Run the backend api instance.
403
468
 
469
+ Raises:
470
+ ValueError: If the app has not been initialized.
471
+
404
472
  Returns:
405
473
  The backend api.
406
474
  """
475
+ if not self.api:
476
+ raise ValueError("The app has not been initialized.")
407
477
  return self.api
408
478
 
409
479
  def _add_default_endpoints(self):
410
480
  """Add default api endpoints (ping)."""
411
481
  # To test the server.
482
+ if not self.api:
483
+ return
484
+
412
485
  self.api.get(str(constants.Endpoint.PING))(ping)
413
486
  self.api.get(str(constants.Endpoint.HEALTH))(health)
414
487
 
415
488
  def _add_optional_endpoints(self):
416
489
  """Add optional api endpoints (_upload)."""
490
+ if not self.api:
491
+ return
492
+
417
493
  if Upload.is_used:
418
494
  # To upload files.
419
495
  self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
@@ -431,6 +507,8 @@ class App(MiddlewareMixin, LifespanMixin):
431
507
 
432
508
  def _add_cors(self):
433
509
  """Add CORS middleware to the app."""
510
+ if not self.api:
511
+ return
434
512
  self.api.add_middleware(
435
513
  cors.CORSMiddleware,
436
514
  allow_credentials=True,
@@ -462,14 +540,8 @@ class App(MiddlewareMixin, LifespanMixin):
462
540
 
463
541
  Returns:
464
542
  The generated component.
465
-
466
- Raises:
467
- exceptions.MatchTypeError: If the return types of match cases in rx.match are different.
468
543
  """
469
- try:
470
- return component if isinstance(component, Component) else component()
471
- except exceptions.MatchTypeError:
472
- raise
544
+ return component if isinstance(component, Component) else component()
473
545
 
474
546
  def add_page(
475
547
  self,
@@ -478,7 +550,7 @@ class App(MiddlewareMixin, LifespanMixin):
478
550
  title: str | Var | None = None,
479
551
  description: str | Var | None = None,
480
552
  image: str = constants.DefaultPage.IMAGE,
481
- on_load: EventType[[], BASE_STATE] | None = None,
553
+ on_load: EventType[()] | None = None,
482
554
  meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
483
555
  ):
484
556
  """Add a page to the app.
@@ -526,13 +598,13 @@ class App(MiddlewareMixin, LifespanMixin):
526
598
  # Check if the route given is valid
527
599
  verify_route_validity(route)
528
600
 
529
- if route in self.unevaluated_pages and environment.RELOAD_CONFIG.is_set():
601
+ if route in self._unevaluated_pages and environment.RELOAD_CONFIG.is_set():
530
602
  # when the app is reloaded(typically for app harness tests), we should maintain
531
603
  # the latest render function of a route.This applies typically to decorated pages
532
604
  # since they are only added when app._compile is called.
533
- self.unevaluated_pages.pop(route)
605
+ self._unevaluated_pages.pop(route)
534
606
 
535
- if route in self.unevaluated_pages:
607
+ if route in self._unevaluated_pages:
536
608
  route_name = (
537
609
  f"`{route}` or `/`"
538
610
  if route == constants.PageNames.INDEX_ROUTE
@@ -545,15 +617,15 @@ class App(MiddlewareMixin, LifespanMixin):
545
617
 
546
618
  # Setup dynamic args for the route.
547
619
  # this state assignment is only required for tests using the deprecated state kwarg for App
548
- state = self.state if self.state else State
620
+ state = self._state if self._state else State
549
621
  state.setup_dynamic_args(get_route_args(route))
550
622
 
551
623
  if on_load:
552
- self.load_events[route] = (
624
+ self._load_events[route] = (
553
625
  on_load if isinstance(on_load, list) else [on_load]
554
626
  )
555
627
 
556
- self.unevaluated_pages[route] = UnevaluatedPage(
628
+ self._unevaluated_pages[route] = UnevaluatedPage(
557
629
  component=component,
558
630
  route=route,
559
631
  title=title,
@@ -563,14 +635,15 @@ class App(MiddlewareMixin, LifespanMixin):
563
635
  meta=meta,
564
636
  )
565
637
 
566
- def _compile_page(self, route: str):
638
+ def _compile_page(self, route: str, save_page: bool = True):
567
639
  """Compile a page.
568
640
 
569
641
  Args:
570
642
  route: The route of the page to compile.
643
+ save_page: If True, the compiled page is saved to self._pages.
571
644
  """
572
645
  component, enable_state = compiler.compile_unevaluated_page(
573
- route, self.unevaluated_pages[route], self.state, self.style, self.theme
646
+ route, self._unevaluated_pages[route], self._state, self.style, self.theme
574
647
  )
575
648
 
576
649
  if enable_state:
@@ -578,9 +651,10 @@ class App(MiddlewareMixin, LifespanMixin):
578
651
 
579
652
  # Add the page.
580
653
  self._check_routes_conflict(route)
581
- self.pages[route] = component
654
+ if save_page:
655
+ self._pages[route] = component
582
656
 
583
- def get_load_events(self, route: str) -> list[IndividualEventType[[], Any]]:
657
+ def get_load_events(self, route: str) -> list[IndividualEventType[()]]:
584
658
  """Get the load events for a route.
585
659
 
586
660
  Args:
@@ -592,7 +666,7 @@ class App(MiddlewareMixin, LifespanMixin):
592
666
  route = route.lstrip("/")
593
667
  if route == "":
594
668
  route = constants.PageNames.INDEX_ROUTE
595
- return self.load_events.get(route, [])
669
+ return self._load_events.get(route, [])
596
670
 
597
671
  def _check_routes_conflict(self, new_route: str):
598
672
  """Verify if there is any conflict between the new route and any existing route.
@@ -616,10 +690,13 @@ class App(MiddlewareMixin, LifespanMixin):
616
690
  constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
617
691
  constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
618
692
  )
619
- for route in self.pages:
693
+ for route in self._pages:
620
694
  replaced_route = replace_brackets_with_keywords(route)
621
695
  for rw, r, nr in zip(
622
- replaced_route.split("/"), route.split("/"), new_route.split("/")
696
+ replaced_route.split("/"),
697
+ route.split("/"),
698
+ new_route.split("/"),
699
+ strict=False,
623
700
  ):
624
701
  if rw in segments and r != nr:
625
702
  # If the slugs in the segments of both routes are not the same, then the route is invalid
@@ -639,7 +716,7 @@ class App(MiddlewareMixin, LifespanMixin):
639
716
  title: str = constants.Page404.TITLE,
640
717
  image: str = constants.Page404.IMAGE,
641
718
  description: str = constants.Page404.DESCRIPTION,
642
- on_load: EventType[[], BASE_STATE] | None = None,
719
+ on_load: EventType[()] | None = None,
643
720
  meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
644
721
  ):
645
722
  """Define a custom 404 page for any url having no match.
@@ -650,8 +727,8 @@ class App(MiddlewareMixin, LifespanMixin):
650
727
  Args:
651
728
  component: The component to display at the page.
652
729
  title: The title of the page.
653
- description: The description of the page.
654
730
  image: The image to display on the page.
731
+ description: The description of the page.
655
732
  on_load: The event handler(s) that will be called each time the page load.
656
733
  meta: The metadata of the page.
657
734
  """
@@ -674,6 +751,9 @@ class App(MiddlewareMixin, LifespanMixin):
674
751
  def _setup_admin_dash(self):
675
752
  """Setup the admin dash."""
676
753
  # Get the admin dash.
754
+ if not self.api:
755
+ return
756
+
677
757
  admin_dash = self.admin_dash
678
758
 
679
759
  if admin_dash and admin_dash.models:
@@ -715,7 +795,7 @@ class App(MiddlewareMixin, LifespanMixin):
715
795
  frontend_packages = get_config().frontend_packages
716
796
  _frontend_packages = []
717
797
  for package in frontend_packages:
718
- if package in (get_config().tailwind or {}).get("plugins", []): # type: ignore
798
+ if package in (get_config().tailwind or {}).get("plugins", []):
719
799
  console.warn(
720
800
  f"Tailwind packages are inferred from 'plugins', remove `{package}` from `frontend_packages`"
721
801
  )
@@ -778,10 +858,10 @@ class App(MiddlewareMixin, LifespanMixin):
778
858
 
779
859
  def _setup_overlay_component(self):
780
860
  """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:
861
+ if self._state is None and self.overlay_component is default_overlay_component:
782
862
  self.overlay_component = None
783
- for k, component in self.pages.items():
784
- self.pages[k] = self._add_overlay_to_component(component)
863
+ for k, component in self._pages.items():
864
+ self._pages[k] = self._add_overlay_to_component(component)
785
865
 
786
866
  def _add_error_boundary_to_component(self, component: Component) -> Component:
787
867
  if self.error_boundary is None:
@@ -793,14 +873,23 @@ class App(MiddlewareMixin, LifespanMixin):
793
873
 
794
874
  def _setup_error_boundary(self):
795
875
  """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:
876
+ if self._state is None and self.error_boundary is default_error_boundary:
797
877
  self.error_boundary = None
798
878
 
799
- for k, component in self.pages.items():
879
+ for k, component in self._pages.items():
800
880
  # Skip the 404 page
801
881
  if k == constants.Page404.SLUG:
802
882
  continue
803
- self.pages[k] = self._add_error_boundary_to_component(component)
883
+ self._pages[k] = self._add_error_boundary_to_component(component)
884
+
885
+ def _setup_sticky_badge(self):
886
+ """Add the sticky badge to the app."""
887
+ for k, component in self._pages.items():
888
+ # Would be nice to share single sticky_badge across all pages, but
889
+ # it bungles the StatefulComponent compile step.
890
+ sticky_badge = sticky()
891
+ sticky_badge._add_style_recursive({})
892
+ self._pages[k] = Fragment.create(sticky_badge, component)
804
893
 
805
894
  def _apply_decorated_pages(self):
806
895
  """Add @rx.page decorated pages to the app.
@@ -826,21 +915,27 @@ class App(MiddlewareMixin, LifespanMixin):
826
915
  Raises:
827
916
  VarDependencyError: When a computed var has an invalid dependency.
828
917
  """
829
- if not self.state:
918
+ if not self._state:
830
919
  return
831
920
 
832
921
  if not state:
833
- state = self.state
922
+ state = self._state
834
923
 
835
924
  for var in state.computed_vars.values():
836
925
  if not var._cache:
837
926
  continue
838
927
  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
- )
928
+ for state_name, dep_set in deps.items():
929
+ state_cls = (
930
+ state.get_root_state().get_class_substate(state_name)
931
+ if state_name != state.get_full_name()
932
+ else state
933
+ )
934
+ for dep in dep_set:
935
+ if dep not in state_cls.vars and dep not in state_cls.backend_vars:
936
+ raise exceptions.VarDependencyError(
937
+ f"ComputedVar {var._js_expr} on state {state.__name__} has an invalid dependency {state_name}.{dep}"
938
+ )
844
939
 
845
940
  for substate in state.class_subclasses:
846
941
  self._validate_var_dependencies(substate)
@@ -856,13 +951,13 @@ class App(MiddlewareMixin, LifespanMixin):
856
951
  """
857
952
  from reflex.utils.exceptions import ReflexRuntimeError
858
953
 
859
- self.pages = {}
954
+ self._pages = {}
860
955
 
861
956
  def get_compilation_time() -> str:
862
957
  return str(datetime.now().time()).split(".")[0]
863
958
 
864
959
  # Render a default 404 page if the user didn't supply one
865
- if constants.Page404.SLUG not in self.unevaluated_pages:
960
+ if constants.Page404.SLUG not in self._unevaluated_pages:
866
961
  self.add_page(route=constants.Page404.SLUG)
867
962
 
868
963
  # Fix up the style.
@@ -878,19 +973,24 @@ class App(MiddlewareMixin, LifespanMixin):
878
973
  # If a theme component was provided, wrap the app with it
879
974
  app_wrappers[(20, "Theme")] = self.theme
880
975
 
881
- for route in self.unevaluated_pages:
882
- console.debug(f"Evaluating page: {route}")
883
- self._compile_page(route)
976
+ # Get the env mode.
977
+ config = get_config()
884
978
 
885
- # Add the optional endpoints (_upload)
886
- self._add_optional_endpoints()
979
+ if config.react_strict_mode:
980
+ app_wrappers[(200, "StrictMode")] = StrictMode.create()
887
981
 
888
- if not self._should_compile():
889
- return
982
+ should_compile = self._should_compile()
890
983
 
891
- self._validate_var_dependencies()
892
- self._setup_overlay_component()
893
- self._setup_error_boundary()
984
+ if not should_compile:
985
+ with console.timing("Evaluate Pages (Backend)"):
986
+ for route in self._unevaluated_pages:
987
+ console.debug(f"Evaluating page: {route}")
988
+ self._compile_page(route, save_page=should_compile)
989
+
990
+ # Add the optional endpoints (_upload)
991
+ self._add_optional_endpoints()
992
+
993
+ return
894
994
 
895
995
  # Create a progress bar.
896
996
  progress = Progress(
@@ -900,18 +1000,33 @@ class App(MiddlewareMixin, LifespanMixin):
900
1000
  )
901
1001
 
902
1002
  # try to be somewhat accurate - but still not 100%
903
- adhoc_steps_without_executor = 6
1003
+ adhoc_steps_without_executor = 7
904
1004
  fixed_pages_within_executor = 5
905
1005
  progress.start()
906
1006
  task = progress.add_task(
907
1007
  f"[{get_compilation_time()}] Compiling:",
908
- total=len(self.pages)
1008
+ total=len(self._pages)
1009
+ + (len(self._unevaluated_pages) * 2)
909
1010
  + fixed_pages_within_executor
910
1011
  + adhoc_steps_without_executor,
911
1012
  )
912
1013
 
913
- # Get the env mode.
914
- config = get_config()
1014
+ with console.timing("Evaluate Pages (Frontend)"):
1015
+ for route in self._unevaluated_pages:
1016
+ console.debug(f"Evaluating page: {route}")
1017
+ self._compile_page(route, save_page=should_compile)
1018
+ progress.advance(task)
1019
+
1020
+ # Add the optional endpoints (_upload)
1021
+ self._add_optional_endpoints()
1022
+
1023
+ self._validate_var_dependencies()
1024
+ self._setup_overlay_component()
1025
+ self._setup_error_boundary()
1026
+ if is_prod_mode() and config.show_built_with_reflex:
1027
+ self._setup_sticky_badge()
1028
+
1029
+ progress.advance(task)
915
1030
 
916
1031
  # Store the compile results.
917
1032
  compile_results = []
@@ -924,7 +1039,7 @@ class App(MiddlewareMixin, LifespanMixin):
924
1039
 
925
1040
  # This has to happen before compiling stateful components as that
926
1041
  # prevents recursive functions from reaching all components.
927
- for component in self.pages.values():
1042
+ for component in self._pages.values():
928
1043
  # Add component._get_all_imports() to all_imports.
929
1044
  all_imports.update(component._get_all_imports())
930
1045
 
@@ -935,16 +1050,16 @@ class App(MiddlewareMixin, LifespanMixin):
935
1050
  custom_components |= component._get_all_custom_components()
936
1051
 
937
1052
  # Perform auto-memoization of stateful components.
938
- (
939
- stateful_components_path,
940
- stateful_components_code,
941
- page_components,
942
- ) = compiler.compile_stateful_components(self.pages.values())
943
-
944
- progress.advance(task)
1053
+ with console.timing("Auto-memoize StatefulComponents"):
1054
+ (
1055
+ stateful_components_path,
1056
+ stateful_components_code,
1057
+ page_components,
1058
+ ) = compiler.compile_stateful_components(self._pages.values())
1059
+ progress.advance(task)
945
1060
 
946
1061
  # 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:
1062
+ if code_uses_state_contexts(stateful_components_code) and self._state is None:
948
1063
  raise ReflexRuntimeError(
949
1064
  "To access rx.State in frontend components, at least one "
950
1065
  "subclass of rx.State must be defined in the app."
@@ -958,12 +1073,23 @@ class App(MiddlewareMixin, LifespanMixin):
958
1073
  compiler.compile_document_root(
959
1074
  self.head_components,
960
1075
  html_lang=self.html_lang,
961
- html_custom_attrs=self.html_custom_attrs, # type: ignore
1076
+ html_custom_attrs=self.html_custom_attrs, # pyright: ignore [reportArgumentType]
962
1077
  )
963
1078
  )
964
1079
 
965
1080
  progress.advance(task)
966
1081
 
1082
+ # Copy the assets.
1083
+ assets_src = Path.cwd() / constants.Dirs.APP_ASSETS
1084
+ if assets_src.is_dir():
1085
+ with console.timing("Copy assets"):
1086
+ path_ops.update_directory_tree(
1087
+ src=assets_src,
1088
+ dest=(
1089
+ Path.cwd() / prerequisites.get_web_dir() / constants.Dirs.PUBLIC
1090
+ ),
1091
+ )
1092
+
967
1093
  # Use a forking process pool, if possible. Much faster, especially for large sites.
968
1094
  # Fallback to ThreadPoolExecutor as something that will always work.
969
1095
  executor = None
@@ -981,20 +1107,20 @@ class App(MiddlewareMixin, LifespanMixin):
981
1107
  max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
982
1108
  )
983
1109
 
984
- for route, component in zip(self.pages, page_components):
1110
+ for route, component in zip(self._pages, page_components, strict=True):
985
1111
  ExecutorSafeFunctions.COMPONENTS[route] = component
986
1112
 
987
- ExecutorSafeFunctions.STATE = self.state
1113
+ ExecutorSafeFunctions.STATE = self._state
988
1114
 
989
1115
  with executor:
990
1116
  result_futures = []
991
1117
 
992
- def _submit_work(fn, *args, **kwargs):
1118
+ def _submit_work(fn: Callable, *args, **kwargs):
993
1119
  f = executor.submit(fn, *args, **kwargs)
994
1120
  result_futures.append(f)
995
1121
 
996
1122
  # Compile the pre-compiled pages.
997
- for route in self.pages:
1123
+ for route in self._pages:
998
1124
  _submit_work(
999
1125
  ExecutorSafeFunctions.compile_page,
1000
1126
  route,
@@ -1016,9 +1142,10 @@ class App(MiddlewareMixin, LifespanMixin):
1016
1142
  _submit_work(compiler.remove_tailwind_from_postcss)
1017
1143
 
1018
1144
  # Wait for all compilation tasks to complete.
1019
- for future in concurrent.futures.as_completed(result_futures):
1020
- compile_results.append(future.result())
1021
- progress.advance(task)
1145
+ with console.timing("Compile to Javascript"):
1146
+ for future in concurrent.futures.as_completed(result_futures):
1147
+ compile_results.append(future.result())
1148
+ progress.advance(task)
1022
1149
 
1023
1150
  app_root = self._app_root(app_wrappers=app_wrappers)
1024
1151
 
@@ -1029,7 +1156,7 @@ class App(MiddlewareMixin, LifespanMixin):
1029
1156
 
1030
1157
  # Compile the contexts.
1031
1158
  compile_results.append(
1032
- compiler.compile_contexts(self.state, self.theme),
1159
+ compiler.compile_contexts(self._state, self.theme),
1033
1160
  )
1034
1161
  if self.theme is not None:
1035
1162
  # Fix #2992 by removing the top-level appearance prop
@@ -1053,7 +1180,8 @@ class App(MiddlewareMixin, LifespanMixin):
1053
1180
  progress.stop()
1054
1181
 
1055
1182
  # Install frontend packages.
1056
- self._get_frontend_packages(all_imports)
1183
+ with console.timing("Install Frontend Packages"):
1184
+ self._get_frontend_packages(all_imports)
1057
1185
 
1058
1186
  # Setup the next.config.js
1059
1187
  transpile_packages = [
@@ -1079,8 +1207,9 @@ class App(MiddlewareMixin, LifespanMixin):
1079
1207
  # Remove pages that are no longer in the app.
1080
1208
  p.unlink()
1081
1209
 
1082
- for output_path, code in compile_results:
1083
- compiler_utils.write_page(output_path, code)
1210
+ with console.timing("Write to Disk"):
1211
+ for output_path, code in compile_results:
1212
+ compiler_utils.write_page(output_path, code)
1084
1213
 
1085
1214
  @contextlib.asynccontextmanager
1086
1215
  async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
@@ -1151,9 +1280,9 @@ class App(MiddlewareMixin, LifespanMixin):
1151
1280
  )
1152
1281
 
1153
1282
  task = asyncio.create_task(_coro())
1154
- self.background_tasks.add(task)
1283
+ self._background_tasks.add(task)
1155
1284
  # Clean up task from background_tasks set when complete.
1156
- task.add_done_callback(self.background_tasks.discard)
1285
+ task.add_done_callback(self._background_tasks.discard)
1157
1286
  return task
1158
1287
 
1159
1288
  def _validate_exception_handlers(self):
@@ -1163,11 +1292,11 @@ class App(MiddlewareMixin, LifespanMixin):
1163
1292
  ValueError: If the custom exception handlers are invalid.
1164
1293
 
1165
1294
  """
1166
- FRONTEND_ARG_SPEC = {
1295
+ frontend_arg_spec = {
1167
1296
  "exception": Exception,
1168
1297
  }
1169
1298
 
1170
- BACKEND_ARG_SPEC = {
1299
+ backend_arg_spec = {
1171
1300
  "exception": Exception,
1172
1301
  }
1173
1302
 
@@ -1175,9 +1304,10 @@ class App(MiddlewareMixin, LifespanMixin):
1175
1304
  ["frontend", "backend"],
1176
1305
  [self.frontend_exception_handler, self.backend_exception_handler],
1177
1306
  [
1178
- FRONTEND_ARG_SPEC,
1179
- BACKEND_ARG_SPEC,
1307
+ frontend_arg_spec,
1308
+ backend_arg_spec,
1180
1309
  ],
1310
+ strict=True,
1181
1311
  ):
1182
1312
  if hasattr(handler_fn, "__name__"):
1183
1313
  _fn_name = handler_fn.__name__
@@ -1218,7 +1348,7 @@ class App(MiddlewareMixin, LifespanMixin):
1218
1348
  ):
1219
1349
  raise ValueError(
1220
1350
  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]}`"
1351
+ f"Expected `{required_arg}` as the {required_arg_index + 1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`"
1222
1352
  )
1223
1353
 
1224
1354
  if not issubclass(arg_annotations[required_arg], Exception):
@@ -1319,15 +1449,14 @@ async def process(
1319
1449
  if app._process_background(state, event) is not None:
1320
1450
  # `final=True` allows the frontend send more events immediately.
1321
1451
  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
1452
+ else:
1453
+ # Process the event synchronously.
1454
+ async for update in state._process(event):
1455
+ # Postprocess the event.
1456
+ update = await app._postprocess(state, event, update)
1457
+
1458
+ # Yield the update.
1459
+ yield update
1331
1460
  except Exception as ex:
1332
1461
  telemetry.send_error(ex, context="backend")
1333
1462
 
@@ -1522,16 +1651,20 @@ class EventNamespace(AsyncNamespace):
1522
1651
  self.sid_to_token = {}
1523
1652
  self.app = app
1524
1653
 
1525
- def on_connect(self, sid, environ):
1654
+ def on_connect(self, sid: str, environ: dict):
1526
1655
  """Event for when the websocket is connected.
1527
1656
 
1528
1657
  Args:
1529
1658
  sid: The Socket.IO session id.
1530
1659
  environ: The request information, including HTTP headers.
1531
1660
  """
1532
- pass
1661
+ subprotocol = environ.get("HTTP_SEC_WEBSOCKET_PROTOCOL")
1662
+ if subprotocol and subprotocol != constants.Reflex.VERSION:
1663
+ console.warn(
1664
+ f"Frontend version {subprotocol} for session {sid} does not match the backend version {constants.Reflex.VERSION}."
1665
+ )
1533
1666
 
1534
- def on_disconnect(self, sid):
1667
+ def on_disconnect(self, sid: str):
1535
1668
  """Event for when the websocket disconnects.
1536
1669
 
1537
1670
  Args:
@@ -1553,7 +1686,7 @@ class EventNamespace(AsyncNamespace):
1553
1686
  self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
1554
1687
  )
1555
1688
 
1556
- async def on_event(self, sid, data):
1689
+ async def on_event(self, sid: str, data: Any):
1557
1690
  """Event for receiving front-end websocket events.
1558
1691
 
1559
1692
  Raises:
@@ -1562,12 +1695,36 @@ class EventNamespace(AsyncNamespace):
1562
1695
  Args:
1563
1696
  sid: The Socket.IO session id.
1564
1697
  data: The event data.
1698
+
1699
+ Raises:
1700
+ EventDeserializationError: If the event data is not a dictionary.
1565
1701
  """
1566
1702
  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
- )
1703
+
1704
+ if isinstance(fields, str):
1705
+ console.warn(
1706
+ "Received event data as a string. This generally should not happen and may indicate a bug."
1707
+ f" Event data: {fields}"
1708
+ )
1709
+ try:
1710
+ fields = json.loads(fields)
1711
+ except json.JSONDecodeError as ex:
1712
+ raise exceptions.EventDeserializationError(
1713
+ f"Failed to deserialize event data: {fields}."
1714
+ ) from ex
1715
+
1716
+ if not isinstance(fields, dict):
1717
+ raise exceptions.EventDeserializationError(
1718
+ f"Event data must be a dictionary, but received {fields} of type {type(fields)}."
1719
+ )
1720
+
1721
+ try:
1722
+ # Get the event.
1723
+ event = Event(**{k: v for k, v in fields.items() if k in _EVENT_FIELDS})
1724
+ except (TypeError, ValueError) as ex:
1725
+ raise exceptions.EventDeserializationError(
1726
+ f"Failed to deserialize event data: {fields}."
1727
+ ) from ex
1571
1728
 
1572
1729
  self.token_to_sid[event.token] = sid
1573
1730
  self.sid_to_token[sid] = event.token
@@ -1596,7 +1753,7 @@ class EventNamespace(AsyncNamespace):
1596
1753
  # Emit the update from processing the event.
1597
1754
  await self.emit_update(update=update, sid=sid)
1598
1755
 
1599
- async def on_ping(self, sid):
1756
+ async def on_ping(self, sid: str):
1600
1757
  """Event for testing the API endpoint.
1601
1758
 
1602
1759
  Args: