reflex 0.6.8a1__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 (248) 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 -135
  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 -20
  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 +228 -233
  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 +34 -39
  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 -21
  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.8a1.dist-info → reflex-0.7.0.dist-info}/METADATA +7 -10
  242. reflex-0.7.0.dist-info/RECORD +401 -0
  243. {reflex-0.6.8a1.dist-info → reflex-0.7.0.dist-info}/WHEEL +1 -1
  244. reflex/experimental/assets.py +0 -37
  245. reflex/proxy.py +0 -119
  246. reflex-0.6.8a1.dist-info/RECORD +0 -398
  247. {reflex-0.6.8a1.dist-info → reflex-0.7.0.dist-info}/LICENSE +0 -0
  248. {reflex-0.6.8a1.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
 
@@ -331,16 +366,10 @@ class App(MiddlewareMixin, LifespanMixin):
331
366
 
332
367
  self.register_lifespan_task(windows_hot_reload_lifespan_hack)
333
368
 
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
369
  def _enable_state(self) -> None:
341
370
  """Enable state for the app."""
342
- if not self.state:
343
- self.state = State
371
+ if not self._state:
372
+ self._state = State
344
373
  self._setup_state()
345
374
 
346
375
  def _setup_state(self) -> None:
@@ -349,13 +378,13 @@ class App(MiddlewareMixin, LifespanMixin):
349
378
  Raises:
350
379
  RuntimeError: If the socket server is invalid.
351
380
  """
352
- if not self.state:
381
+ if not self._state:
353
382
  return
354
383
 
355
384
  config = get_config()
356
385
 
357
386
  # Set up the state manager.
358
- self._state_manager = StateManager.create(state=self.state)
387
+ self._state_manager = StateManager.create(state=self._state)
359
388
 
360
389
  # Set up the Socket.IO AsyncServer.
361
390
  if not self.sio:
@@ -386,12 +415,42 @@ class App(MiddlewareMixin, LifespanMixin):
386
415
  namespace = config.get_event_namespace()
387
416
 
388
417
  # Create the event namespace and attach the main app. Not related to any paths.
389
- self.event_namespace = EventNamespace(namespace, self)
418
+ self._event_namespace = EventNamespace(namespace, self)
390
419
 
391
420
  # Register the event namespace with the socket.
392
421
  self.sio.register_namespace(self.event_namespace)
393
422
  # Mount the socket app with the API.
394
- 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)
395
454
 
396
455
  # Check the exception handlers
397
456
  self._validate_exception_handlers()
@@ -402,24 +461,35 @@ class App(MiddlewareMixin, LifespanMixin):
402
461
  Returns:
403
462
  The string representation of the app.
404
463
  """
405
- 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}>"
406
465
 
407
466
  def __call__(self) -> FastAPI:
408
467
  """Run the backend api instance.
409
468
 
469
+ Raises:
470
+ ValueError: If the app has not been initialized.
471
+
410
472
  Returns:
411
473
  The backend api.
412
474
  """
475
+ if not self.api:
476
+ raise ValueError("The app has not been initialized.")
413
477
  return self.api
414
478
 
415
479
  def _add_default_endpoints(self):
416
480
  """Add default api endpoints (ping)."""
417
481
  # To test the server.
482
+ if not self.api:
483
+ return
484
+
418
485
  self.api.get(str(constants.Endpoint.PING))(ping)
419
486
  self.api.get(str(constants.Endpoint.HEALTH))(health)
420
487
 
421
488
  def _add_optional_endpoints(self):
422
489
  """Add optional api endpoints (_upload)."""
490
+ if not self.api:
491
+ return
492
+
423
493
  if Upload.is_used:
424
494
  # To upload files.
425
495
  self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
@@ -437,6 +507,8 @@ class App(MiddlewareMixin, LifespanMixin):
437
507
 
438
508
  def _add_cors(self):
439
509
  """Add CORS middleware to the app."""
510
+ if not self.api:
511
+ return
440
512
  self.api.add_middleware(
441
513
  cors.CORSMiddleware,
442
514
  allow_credentials=True,
@@ -468,14 +540,8 @@ class App(MiddlewareMixin, LifespanMixin):
468
540
 
469
541
  Returns:
470
542
  The generated component.
471
-
472
- Raises:
473
- exceptions.MatchTypeError: If the return types of match cases in rx.match are different.
474
543
  """
475
- try:
476
- return component if isinstance(component, Component) else component()
477
- except exceptions.MatchTypeError:
478
- raise
544
+ return component if isinstance(component, Component) else component()
479
545
 
480
546
  def add_page(
481
547
  self,
@@ -484,7 +550,7 @@ class App(MiddlewareMixin, LifespanMixin):
484
550
  title: str | Var | None = None,
485
551
  description: str | Var | None = None,
486
552
  image: str = constants.DefaultPage.IMAGE,
487
- on_load: EventType[[], BASE_STATE] | None = None,
553
+ on_load: EventType[()] | None = None,
488
554
  meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
489
555
  ):
490
556
  """Add a page to the app.
@@ -532,13 +598,13 @@ class App(MiddlewareMixin, LifespanMixin):
532
598
  # Check if the route given is valid
533
599
  verify_route_validity(route)
534
600
 
535
- 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():
536
602
  # when the app is reloaded(typically for app harness tests), we should maintain
537
603
  # the latest render function of a route.This applies typically to decorated pages
538
604
  # since they are only added when app._compile is called.
539
- self.unevaluated_pages.pop(route)
605
+ self._unevaluated_pages.pop(route)
540
606
 
541
- if route in self.unevaluated_pages:
607
+ if route in self._unevaluated_pages:
542
608
  route_name = (
543
609
  f"`{route}` or `/`"
544
610
  if route == constants.PageNames.INDEX_ROUTE
@@ -551,15 +617,15 @@ class App(MiddlewareMixin, LifespanMixin):
551
617
 
552
618
  # Setup dynamic args for the route.
553
619
  # this state assignment is only required for tests using the deprecated state kwarg for App
554
- state = self.state if self.state else State
620
+ state = self._state if self._state else State
555
621
  state.setup_dynamic_args(get_route_args(route))
556
622
 
557
623
  if on_load:
558
- self.load_events[route] = (
624
+ self._load_events[route] = (
559
625
  on_load if isinstance(on_load, list) else [on_load]
560
626
  )
561
627
 
562
- self.unevaluated_pages[route] = UnevaluatedPage(
628
+ self._unevaluated_pages[route] = UnevaluatedPage(
563
629
  component=component,
564
630
  route=route,
565
631
  title=title,
@@ -569,14 +635,15 @@ class App(MiddlewareMixin, LifespanMixin):
569
635
  meta=meta,
570
636
  )
571
637
 
572
- def _compile_page(self, route: str):
638
+ def _compile_page(self, route: str, save_page: bool = True):
573
639
  """Compile a page.
574
640
 
575
641
  Args:
576
642
  route: The route of the page to compile.
643
+ save_page: If True, the compiled page is saved to self._pages.
577
644
  """
578
645
  component, enable_state = compiler.compile_unevaluated_page(
579
- route, self.unevaluated_pages[route], self.state, self.style, self.theme
646
+ route, self._unevaluated_pages[route], self._state, self.style, self.theme
580
647
  )
581
648
 
582
649
  if enable_state:
@@ -584,9 +651,10 @@ class App(MiddlewareMixin, LifespanMixin):
584
651
 
585
652
  # Add the page.
586
653
  self._check_routes_conflict(route)
587
- self.pages[route] = component
654
+ if save_page:
655
+ self._pages[route] = component
588
656
 
589
- def get_load_events(self, route: str) -> list[IndividualEventType[[], Any]]:
657
+ def get_load_events(self, route: str) -> list[IndividualEventType[()]]:
590
658
  """Get the load events for a route.
591
659
 
592
660
  Args:
@@ -598,7 +666,7 @@ class App(MiddlewareMixin, LifespanMixin):
598
666
  route = route.lstrip("/")
599
667
  if route == "":
600
668
  route = constants.PageNames.INDEX_ROUTE
601
- return self.load_events.get(route, [])
669
+ return self._load_events.get(route, [])
602
670
 
603
671
  def _check_routes_conflict(self, new_route: str):
604
672
  """Verify if there is any conflict between the new route and any existing route.
@@ -622,10 +690,13 @@ class App(MiddlewareMixin, LifespanMixin):
622
690
  constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
623
691
  constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
624
692
  )
625
- for route in self.pages:
693
+ for route in self._pages:
626
694
  replaced_route = replace_brackets_with_keywords(route)
627
695
  for rw, r, nr in zip(
628
- replaced_route.split("/"), route.split("/"), new_route.split("/")
696
+ replaced_route.split("/"),
697
+ route.split("/"),
698
+ new_route.split("/"),
699
+ strict=False,
629
700
  ):
630
701
  if rw in segments and r != nr:
631
702
  # If the slugs in the segments of both routes are not the same, then the route is invalid
@@ -645,7 +716,7 @@ class App(MiddlewareMixin, LifespanMixin):
645
716
  title: str = constants.Page404.TITLE,
646
717
  image: str = constants.Page404.IMAGE,
647
718
  description: str = constants.Page404.DESCRIPTION,
648
- on_load: EventType[[], BASE_STATE] | None = None,
719
+ on_load: EventType[()] | None = None,
649
720
  meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
650
721
  ):
651
722
  """Define a custom 404 page for any url having no match.
@@ -656,8 +727,8 @@ class App(MiddlewareMixin, LifespanMixin):
656
727
  Args:
657
728
  component: The component to display at the page.
658
729
  title: The title of the page.
659
- description: The description of the page.
660
730
  image: The image to display on the page.
731
+ description: The description of the page.
661
732
  on_load: The event handler(s) that will be called each time the page load.
662
733
  meta: The metadata of the page.
663
734
  """
@@ -680,6 +751,9 @@ class App(MiddlewareMixin, LifespanMixin):
680
751
  def _setup_admin_dash(self):
681
752
  """Setup the admin dash."""
682
753
  # Get the admin dash.
754
+ if not self.api:
755
+ return
756
+
683
757
  admin_dash = self.admin_dash
684
758
 
685
759
  if admin_dash and admin_dash.models:
@@ -721,7 +795,7 @@ class App(MiddlewareMixin, LifespanMixin):
721
795
  frontend_packages = get_config().frontend_packages
722
796
  _frontend_packages = []
723
797
  for package in frontend_packages:
724
- if package in (get_config().tailwind or {}).get("plugins", []): # type: ignore
798
+ if package in (get_config().tailwind or {}).get("plugins", []):
725
799
  console.warn(
726
800
  f"Tailwind packages are inferred from 'plugins', remove `{package}` from `frontend_packages`"
727
801
  )
@@ -784,10 +858,10 @@ class App(MiddlewareMixin, LifespanMixin):
784
858
 
785
859
  def _setup_overlay_component(self):
786
860
  """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:
861
+ if self._state is None and self.overlay_component is default_overlay_component:
788
862
  self.overlay_component = None
789
- for k, component in self.pages.items():
790
- 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)
791
865
 
792
866
  def _add_error_boundary_to_component(self, component: Component) -> Component:
793
867
  if self.error_boundary is None:
@@ -799,14 +873,23 @@ class App(MiddlewareMixin, LifespanMixin):
799
873
 
800
874
  def _setup_error_boundary(self):
801
875
  """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:
876
+ if self._state is None and self.error_boundary is default_error_boundary:
803
877
  self.error_boundary = None
804
878
 
805
- for k, component in self.pages.items():
879
+ for k, component in self._pages.items():
806
880
  # Skip the 404 page
807
881
  if k == constants.Page404.SLUG:
808
882
  continue
809
- 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)
810
893
 
811
894
  def _apply_decorated_pages(self):
812
895
  """Add @rx.page decorated pages to the app.
@@ -832,21 +915,27 @@ class App(MiddlewareMixin, LifespanMixin):
832
915
  Raises:
833
916
  VarDependencyError: When a computed var has an invalid dependency.
834
917
  """
835
- if not self.state:
918
+ if not self._state:
836
919
  return
837
920
 
838
921
  if not state:
839
- state = self.state
922
+ state = self._state
840
923
 
841
924
  for var in state.computed_vars.values():
842
925
  if not var._cache:
843
926
  continue
844
927
  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
- )
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
+ )
850
939
 
851
940
  for substate in state.class_subclasses:
852
941
  self._validate_var_dependencies(substate)
@@ -862,13 +951,13 @@ class App(MiddlewareMixin, LifespanMixin):
862
951
  """
863
952
  from reflex.utils.exceptions import ReflexRuntimeError
864
953
 
865
- self.pages = {}
954
+ self._pages = {}
866
955
 
867
956
  def get_compilation_time() -> str:
868
957
  return str(datetime.now().time()).split(".")[0]
869
958
 
870
959
  # Render a default 404 page if the user didn't supply one
871
- if constants.Page404.SLUG not in self.unevaluated_pages:
960
+ if constants.Page404.SLUG not in self._unevaluated_pages:
872
961
  self.add_page(route=constants.Page404.SLUG)
873
962
 
874
963
  # Fix up the style.
@@ -884,19 +973,24 @@ class App(MiddlewareMixin, LifespanMixin):
884
973
  # If a theme component was provided, wrap the app with it
885
974
  app_wrappers[(20, "Theme")] = self.theme
886
975
 
887
- for route in self.unevaluated_pages:
888
- console.debug(f"Evaluating page: {route}")
889
- self._compile_page(route)
976
+ # Get the env mode.
977
+ config = get_config()
890
978
 
891
- # Add the optional endpoints (_upload)
892
- self._add_optional_endpoints()
979
+ if config.react_strict_mode:
980
+ app_wrappers[(200, "StrictMode")] = StrictMode.create()
893
981
 
894
- if not self._should_compile():
895
- return
982
+ should_compile = self._should_compile()
896
983
 
897
- self._validate_var_dependencies()
898
- self._setup_overlay_component()
899
- 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
900
994
 
901
995
  # Create a progress bar.
902
996
  progress = Progress(
@@ -906,18 +1000,33 @@ class App(MiddlewareMixin, LifespanMixin):
906
1000
  )
907
1001
 
908
1002
  # try to be somewhat accurate - but still not 100%
909
- adhoc_steps_without_executor = 6
1003
+ adhoc_steps_without_executor = 7
910
1004
  fixed_pages_within_executor = 5
911
1005
  progress.start()
912
1006
  task = progress.add_task(
913
1007
  f"[{get_compilation_time()}] Compiling:",
914
- total=len(self.pages)
1008
+ total=len(self._pages)
1009
+ + (len(self._unevaluated_pages) * 2)
915
1010
  + fixed_pages_within_executor
916
1011
  + adhoc_steps_without_executor,
917
1012
  )
918
1013
 
919
- # Get the env mode.
920
- 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)
921
1030
 
922
1031
  # Store the compile results.
923
1032
  compile_results = []
@@ -930,7 +1039,7 @@ class App(MiddlewareMixin, LifespanMixin):
930
1039
 
931
1040
  # This has to happen before compiling stateful components as that
932
1041
  # prevents recursive functions from reaching all components.
933
- for component in self.pages.values():
1042
+ for component in self._pages.values():
934
1043
  # Add component._get_all_imports() to all_imports.
935
1044
  all_imports.update(component._get_all_imports())
936
1045
 
@@ -941,16 +1050,16 @@ class App(MiddlewareMixin, LifespanMixin):
941
1050
  custom_components |= component._get_all_custom_components()
942
1051
 
943
1052
  # Perform auto-memoization of stateful components.
944
- (
945
- stateful_components_path,
946
- stateful_components_code,
947
- page_components,
948
- ) = compiler.compile_stateful_components(self.pages.values())
949
-
950
- 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)
951
1060
 
952
1061
  # 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:
1062
+ if code_uses_state_contexts(stateful_components_code) and self._state is None:
954
1063
  raise ReflexRuntimeError(
955
1064
  "To access rx.State in frontend components, at least one "
956
1065
  "subclass of rx.State must be defined in the app."
@@ -964,12 +1073,23 @@ class App(MiddlewareMixin, LifespanMixin):
964
1073
  compiler.compile_document_root(
965
1074
  self.head_components,
966
1075
  html_lang=self.html_lang,
967
- html_custom_attrs=self.html_custom_attrs, # type: ignore
1076
+ html_custom_attrs=self.html_custom_attrs, # pyright: ignore [reportArgumentType]
968
1077
  )
969
1078
  )
970
1079
 
971
1080
  progress.advance(task)
972
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
+
973
1093
  # Use a forking process pool, if possible. Much faster, especially for large sites.
974
1094
  # Fallback to ThreadPoolExecutor as something that will always work.
975
1095
  executor = None
@@ -987,20 +1107,20 @@ class App(MiddlewareMixin, LifespanMixin):
987
1107
  max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
988
1108
  )
989
1109
 
990
- for route, component in zip(self.pages, page_components):
1110
+ for route, component in zip(self._pages, page_components, strict=True):
991
1111
  ExecutorSafeFunctions.COMPONENTS[route] = component
992
1112
 
993
- ExecutorSafeFunctions.STATE = self.state
1113
+ ExecutorSafeFunctions.STATE = self._state
994
1114
 
995
1115
  with executor:
996
1116
  result_futures = []
997
1117
 
998
- def _submit_work(fn, *args, **kwargs):
1118
+ def _submit_work(fn: Callable, *args, **kwargs):
999
1119
  f = executor.submit(fn, *args, **kwargs)
1000
1120
  result_futures.append(f)
1001
1121
 
1002
1122
  # Compile the pre-compiled pages.
1003
- for route in self.pages:
1123
+ for route in self._pages:
1004
1124
  _submit_work(
1005
1125
  ExecutorSafeFunctions.compile_page,
1006
1126
  route,
@@ -1022,9 +1142,10 @@ class App(MiddlewareMixin, LifespanMixin):
1022
1142
  _submit_work(compiler.remove_tailwind_from_postcss)
1023
1143
 
1024
1144
  # Wait for all compilation tasks to complete.
1025
- for future in concurrent.futures.as_completed(result_futures):
1026
- compile_results.append(future.result())
1027
- 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)
1028
1149
 
1029
1150
  app_root = self._app_root(app_wrappers=app_wrappers)
1030
1151
 
@@ -1035,7 +1156,7 @@ class App(MiddlewareMixin, LifespanMixin):
1035
1156
 
1036
1157
  # Compile the contexts.
1037
1158
  compile_results.append(
1038
- compiler.compile_contexts(self.state, self.theme),
1159
+ compiler.compile_contexts(self._state, self.theme),
1039
1160
  )
1040
1161
  if self.theme is not None:
1041
1162
  # Fix #2992 by removing the top-level appearance prop
@@ -1059,7 +1180,8 @@ class App(MiddlewareMixin, LifespanMixin):
1059
1180
  progress.stop()
1060
1181
 
1061
1182
  # Install frontend packages.
1062
- self._get_frontend_packages(all_imports)
1183
+ with console.timing("Install Frontend Packages"):
1184
+ self._get_frontend_packages(all_imports)
1063
1185
 
1064
1186
  # Setup the next.config.js
1065
1187
  transpile_packages = [
@@ -1085,8 +1207,9 @@ class App(MiddlewareMixin, LifespanMixin):
1085
1207
  # Remove pages that are no longer in the app.
1086
1208
  p.unlink()
1087
1209
 
1088
- for output_path, code in compile_results:
1089
- 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)
1090
1213
 
1091
1214
  @contextlib.asynccontextmanager
1092
1215
  async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
@@ -1157,9 +1280,9 @@ class App(MiddlewareMixin, LifespanMixin):
1157
1280
  )
1158
1281
 
1159
1282
  task = asyncio.create_task(_coro())
1160
- self.background_tasks.add(task)
1283
+ self._background_tasks.add(task)
1161
1284
  # Clean up task from background_tasks set when complete.
1162
- task.add_done_callback(self.background_tasks.discard)
1285
+ task.add_done_callback(self._background_tasks.discard)
1163
1286
  return task
1164
1287
 
1165
1288
  def _validate_exception_handlers(self):
@@ -1169,11 +1292,11 @@ class App(MiddlewareMixin, LifespanMixin):
1169
1292
  ValueError: If the custom exception handlers are invalid.
1170
1293
 
1171
1294
  """
1172
- FRONTEND_ARG_SPEC = {
1295
+ frontend_arg_spec = {
1173
1296
  "exception": Exception,
1174
1297
  }
1175
1298
 
1176
- BACKEND_ARG_SPEC = {
1299
+ backend_arg_spec = {
1177
1300
  "exception": Exception,
1178
1301
  }
1179
1302
 
@@ -1181,9 +1304,10 @@ class App(MiddlewareMixin, LifespanMixin):
1181
1304
  ["frontend", "backend"],
1182
1305
  [self.frontend_exception_handler, self.backend_exception_handler],
1183
1306
  [
1184
- FRONTEND_ARG_SPEC,
1185
- BACKEND_ARG_SPEC,
1307
+ frontend_arg_spec,
1308
+ backend_arg_spec,
1186
1309
  ],
1310
+ strict=True,
1187
1311
  ):
1188
1312
  if hasattr(handler_fn, "__name__"):
1189
1313
  _fn_name = handler_fn.__name__
@@ -1224,7 +1348,7 @@ class App(MiddlewareMixin, LifespanMixin):
1224
1348
  ):
1225
1349
  raise ValueError(
1226
1350
  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]}`"
1351
+ f"Expected `{required_arg}` as the {required_arg_index + 1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`"
1228
1352
  )
1229
1353
 
1230
1354
  if not issubclass(arg_annotations[required_arg], Exception):
@@ -1325,15 +1449,14 @@ async def process(
1325
1449
  if app._process_background(state, event) is not None:
1326
1450
  # `final=True` allows the frontend send more events immediately.
1327
1451
  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
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
1337
1460
  except Exception as ex:
1338
1461
  telemetry.send_error(ex, context="backend")
1339
1462
 
@@ -1528,16 +1651,20 @@ class EventNamespace(AsyncNamespace):
1528
1651
  self.sid_to_token = {}
1529
1652
  self.app = app
1530
1653
 
1531
- def on_connect(self, sid, environ):
1654
+ def on_connect(self, sid: str, environ: dict):
1532
1655
  """Event for when the websocket is connected.
1533
1656
 
1534
1657
  Args:
1535
1658
  sid: The Socket.IO session id.
1536
1659
  environ: The request information, including HTTP headers.
1537
1660
  """
1538
- 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
+ )
1539
1666
 
1540
- def on_disconnect(self, sid):
1667
+ def on_disconnect(self, sid: str):
1541
1668
  """Event for when the websocket disconnects.
1542
1669
 
1543
1670
  Args:
@@ -1559,7 +1686,7 @@ class EventNamespace(AsyncNamespace):
1559
1686
  self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
1560
1687
  )
1561
1688
 
1562
- async def on_event(self, sid, data):
1689
+ async def on_event(self, sid: str, data: Any):
1563
1690
  """Event for receiving front-end websocket events.
1564
1691
 
1565
1692
  Raises:
@@ -1568,12 +1695,36 @@ class EventNamespace(AsyncNamespace):
1568
1695
  Args:
1569
1696
  sid: The Socket.IO session id.
1570
1697
  data: The event data.
1698
+
1699
+ Raises:
1700
+ EventDeserializationError: If the event data is not a dictionary.
1571
1701
  """
1572
1702
  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
- )
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
1577
1728
 
1578
1729
  self.token_to_sid[event.token] = sid
1579
1730
  self.sid_to_token[sid] = event.token
@@ -1602,7 +1753,7 @@ class EventNamespace(AsyncNamespace):
1602
1753
  # Emit the update from processing the event.
1603
1754
  await self.emit_update(update=update, sid=sid)
1604
1755
 
1605
- async def on_ping(self, sid):
1756
+ async def on_ping(self, sid: str):
1606
1757
  """Event for testing the API endpoint.
1607
1758
 
1608
1759
  Args: