reflex 0.6.6.post2__py3-none-any.whl → 0.6.7__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 (141) hide show
  1. reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +5 -1
  2. reflex/.templates/web/utils/state.js +36 -28
  3. reflex/__init__.py +1 -1
  4. reflex/__init__.pyi +1 -0
  5. reflex/app.py +41 -16
  6. reflex/assets.py +2 -2
  7. reflex/base.py +8 -7
  8. reflex/compiler/templates.py +1 -0
  9. reflex/compiler/utils.py +2 -3
  10. reflex/components/base/bare.py +2 -2
  11. reflex/components/component.py +54 -29
  12. reflex/components/core/banner.py +2 -2
  13. reflex/components/core/banner.pyi +1 -1
  14. reflex/components/core/client_side_routing.py +2 -2
  15. reflex/components/core/client_side_routing.pyi +1 -1
  16. reflex/components/core/clipboard.py +11 -9
  17. reflex/components/core/clipboard.pyi +1 -1
  18. reflex/components/core/cond.py +3 -3
  19. reflex/components/core/foreach.py +1 -1
  20. reflex/components/core/html.pyi +1 -1
  21. reflex/components/core/upload.py +8 -8
  22. reflex/components/datadisplay/code.py +5 -5
  23. reflex/components/datadisplay/dataeditor.py +8 -28
  24. reflex/components/datadisplay/dataeditor.pyi +1 -1
  25. reflex/components/datadisplay/shiki_code_block.py +7 -7
  26. reflex/components/dynamic.py +2 -2
  27. reflex/components/el/elements/__init__.py +1 -1
  28. reflex/components/el/elements/__init__.pyi +1 -1
  29. reflex/components/el/elements/base.py +2 -2
  30. reflex/components/el/elements/base.pyi +1 -1
  31. reflex/components/el/elements/forms.py +40 -10
  32. reflex/components/el/elements/forms.pyi +17 -15
  33. reflex/components/el/elements/inline.py +1 -1
  34. reflex/components/el/elements/inline.pyi +28 -28
  35. reflex/components/el/elements/media.py +1 -4
  36. reflex/components/el/elements/media.pyi +25 -26
  37. reflex/components/el/elements/metadata.py +6 -6
  38. reflex/components/el/elements/metadata.pyi +4 -4
  39. reflex/components/el/elements/other.py +17 -9
  40. reflex/components/el/elements/other.pyi +7 -7
  41. reflex/components/el/elements/scripts.py +1 -2
  42. reflex/components/el/elements/scripts.pyi +3 -3
  43. reflex/components/el/elements/sectioning.py +16 -16
  44. reflex/components/el/elements/sectioning.pyi +15 -15
  45. reflex/components/el/elements/tables.py +1 -1
  46. reflex/components/el/elements/tables.pyi +10 -10
  47. reflex/components/el/elements/typography.py +1 -1
  48. reflex/components/el/elements/typography.pyi +15 -15
  49. reflex/components/markdown/markdown.py +3 -3
  50. reflex/components/next/image.py +1 -1
  51. reflex/components/next/image.pyi +1 -1
  52. reflex/components/plotly/plotly.py +2 -2
  53. reflex/components/radix/primitives/accordion.py +2 -1
  54. reflex/components/radix/primitives/form.pyi +3 -3
  55. reflex/components/radix/primitives/slider.py +1 -1
  56. reflex/components/radix/themes/base.py +4 -10
  57. reflex/components/radix/themes/color_mode.pyi +2 -2
  58. reflex/components/radix/themes/components/alert_dialog.pyi +1 -1
  59. reflex/components/radix/themes/components/badge.pyi +1 -1
  60. reflex/components/radix/themes/components/button.pyi +1 -1
  61. reflex/components/radix/themes/components/callout.pyi +5 -5
  62. reflex/components/radix/themes/components/card.pyi +1 -1
  63. reflex/components/radix/themes/components/checkbox.pyi +3 -3
  64. reflex/components/radix/themes/components/context_menu.py +11 -0
  65. reflex/components/radix/themes/components/context_menu.pyi +155 -0
  66. reflex/components/radix/themes/components/dialog.pyi +1 -1
  67. reflex/components/radix/themes/components/hover_card.pyi +1 -1
  68. reflex/components/radix/themes/components/icon_button.py +1 -1
  69. reflex/components/radix/themes/components/icon_button.pyi +1 -1
  70. reflex/components/radix/themes/components/inset.pyi +1 -1
  71. reflex/components/radix/themes/components/popover.pyi +1 -1
  72. reflex/components/radix/themes/components/radio_group.py +2 -4
  73. reflex/components/radix/themes/components/radio_group.pyi +1 -1
  74. reflex/components/radix/themes/components/select.pyi +3 -3
  75. reflex/components/radix/themes/components/slider.pyi +1 -1
  76. reflex/components/radix/themes/components/switch.pyi +1 -1
  77. reflex/components/radix/themes/components/table.pyi +7 -7
  78. reflex/components/radix/themes/components/tabs.pyi +2 -2
  79. reflex/components/radix/themes/components/text_area.py +3 -0
  80. reflex/components/radix/themes/components/text_area.pyi +3 -1
  81. reflex/components/radix/themes/components/text_field.py +16 -1
  82. reflex/components/radix/themes/components/text_field.pyi +105 -17
  83. reflex/components/radix/themes/layout/box.pyi +1 -1
  84. reflex/components/radix/themes/layout/center.pyi +1 -1
  85. reflex/components/radix/themes/layout/flex.pyi +1 -1
  86. reflex/components/radix/themes/layout/grid.pyi +1 -1
  87. reflex/components/radix/themes/layout/list.py +0 -4
  88. reflex/components/radix/themes/layout/list.pyi +3 -8
  89. reflex/components/radix/themes/layout/section.pyi +1 -1
  90. reflex/components/radix/themes/layout/spacer.pyi +1 -1
  91. reflex/components/radix/themes/layout/stack.pyi +3 -3
  92. reflex/components/radix/themes/typography/blockquote.pyi +1 -1
  93. reflex/components/radix/themes/typography/code.pyi +1 -1
  94. reflex/components/radix/themes/typography/heading.pyi +1 -1
  95. reflex/components/radix/themes/typography/link.py +5 -1
  96. reflex/components/radix/themes/typography/link.pyi +1 -1
  97. reflex/components/radix/themes/typography/text.pyi +7 -7
  98. reflex/components/recharts/cartesian.py +1 -1
  99. reflex/components/recharts/charts.py +4 -4
  100. reflex/components/recharts/polar.py +1 -1
  101. reflex/components/recharts/polar.pyi +1 -1
  102. reflex/components/sonner/toast.py +4 -7
  103. reflex/components/suneditor/editor.py +6 -6
  104. reflex/components/suneditor/editor.pyi +6 -6
  105. reflex/config.py +25 -10
  106. reflex/constants/compiler.py +6 -0
  107. reflex/constants/config.py +2 -0
  108. reflex/constants/custom_components.py +1 -1
  109. reflex/constants/route.py +1 -1
  110. reflex/custom_components/custom_components.py +21 -21
  111. reflex/event.py +57 -22
  112. reflex/experimental/client_state.py +2 -1
  113. reflex/experimental/layout.py +0 -6
  114. reflex/model.py +125 -9
  115. reflex/reflex.py +12 -8
  116. reflex/state.py +200 -88
  117. reflex/style.py +1 -4
  118. reflex/testing.py +10 -11
  119. reflex/utils/build.py +1 -1
  120. reflex/utils/console.py +75 -6
  121. reflex/utils/exceptions.py +12 -0
  122. reflex/utils/exec.py +10 -10
  123. reflex/utils/export.py +1 -2
  124. reflex/utils/format.py +11 -8
  125. reflex/utils/path_ops.py +2 -2
  126. reflex/utils/prerequisites.py +31 -28
  127. reflex/utils/processes.py +4 -4
  128. reflex/utils/pyi_generator.py +12 -11
  129. reflex/utils/types.py +6 -3
  130. reflex/vars/__init__.py +1 -0
  131. reflex/vars/base.py +75 -38
  132. reflex/vars/datetime.py +222 -0
  133. reflex/vars/function.py +3 -3
  134. reflex/vars/number.py +3 -3
  135. reflex/vars/object.py +5 -5
  136. reflex/vars/sequence.py +7 -7
  137. {reflex-0.6.6.post2.dist-info → reflex-0.6.7.dist-info}/METADATA +2 -2
  138. {reflex-0.6.6.post2.dist-info → reflex-0.6.7.dist-info}/RECORD +141 -140
  139. {reflex-0.6.6.post2.dist-info → reflex-0.6.7.dist-info}/LICENSE +0 -0
  140. {reflex-0.6.6.post2.dist-info → reflex-0.6.7.dist-info}/WHEEL +0 -0
  141. {reflex-0.6.6.post2.dist-info → reflex-0.6.7.dist-info}/entry_points.txt +0 -0
@@ -5,11 +5,15 @@ export function {{tag_name}} () {
5
5
  {{ hook }}
6
6
  {% endfor %}
7
7
 
8
+ {% for hook, data in component._get_all_hooks().items() if not data.position or data.position == const.hook_position.PRE_TRIGGER %}
9
+ {{ hook }}
10
+ {% endfor %}
11
+
8
12
  {% for hook in memo_trigger_hooks %}
9
13
  {{ hook }}
10
14
  {% endfor %}
11
15
 
12
- {% for hook in component._get_all_hooks() %}
16
+ {% for hook, data in component._get_all_hooks().items() if data.position and data.position == const.hook_position.POST_TRIGGER %}
13
17
  {{ hook }}
14
18
  {% endfor %}
15
19
 
@@ -40,9 +40,6 @@ let event_processing = false;
40
40
  // Array holding pending events to be processed.
41
41
  const event_queue = [];
42
42
 
43
- // Pending upload promises, by id
44
- const upload_controllers = {};
45
-
46
43
  /**
47
44
  * Generate a UUID (Used for session tokens).
48
45
  * Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
@@ -300,7 +297,7 @@ export const applyEvent = async (event, socket) => {
300
297
  if (socket) {
301
298
  socket.emit(
302
299
  "event",
303
- JSON.stringify(event, (k, v) => (v === undefined ? null : v))
300
+ event,
304
301
  );
305
302
  return true;
306
303
  }
@@ -407,6 +404,8 @@ export const connect = async (
407
404
  transports: transports,
408
405
  autoUnref: false,
409
406
  });
407
+ // Ensure undefined fields in events are sent as null instead of removed
408
+ socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v)
410
409
 
411
410
  function checkVisibility() {
412
411
  if (document.visibilityState === "visible") {
@@ -443,8 +442,7 @@ export const connect = async (
443
442
  });
444
443
 
445
444
  // On each received message, queue the updates and events.
446
- socket.current.on("event", async (message) => {
447
- const update = JSON5.parse(message);
445
+ socket.current.on("event", async (update) => {
448
446
  for (const substate in update.delta) {
449
447
  dispatch[substate](update.delta[substate]);
450
448
  }
@@ -456,7 +454,7 @@ export const connect = async (
456
454
  });
457
455
  socket.current.on("reload", async (event) => {
458
456
  event_processing = false;
459
- queueEvents([...initialEvents(), JSON5.parse(event)], socket);
457
+ queueEvents([...initialEvents(), event], socket);
460
458
  });
461
459
 
462
460
  document.addEventListener("visibilitychange", checkVisibility);
@@ -485,7 +483,9 @@ export const uploadFiles = async (
485
483
  return false;
486
484
  }
487
485
 
488
- if (upload_controllers[upload_id]) {
486
+ const upload_ref_name = `__upload_controllers_${upload_id}`
487
+
488
+ if (refs[upload_ref_name]) {
489
489
  console.log("Upload already in progress for ", upload_id);
490
490
  return false;
491
491
  }
@@ -497,23 +497,31 @@ export const uploadFiles = async (
497
497
  // Whenever called, responseText will contain the entire response so far.
498
498
  const chunks = progressEvent.event.target.responseText.trim().split("\n");
499
499
  // So only process _new_ chunks beyond resp_idx.
500
- chunks.slice(resp_idx).map((chunk) => {
501
- event_callbacks.map((f, ix) => {
502
- f(chunk)
503
- .then(() => {
504
- if (ix === event_callbacks.length - 1) {
505
- // Mark this chunk as processed.
506
- resp_idx += 1;
507
- }
508
- })
509
- .catch((e) => {
510
- if (progressEvent.progress === 1) {
511
- // Chunk may be incomplete, so only report errors when full response is available.
512
- console.log("Error parsing chunk", chunk, e);
513
- }
514
- return;
515
- });
516
- });
500
+ chunks.slice(resp_idx).map((chunk_json) => {
501
+ try {
502
+ const chunk = JSON5.parse(chunk_json);
503
+ event_callbacks.map((f, ix) => {
504
+ f(chunk)
505
+ .then(() => {
506
+ if (ix === event_callbacks.length - 1) {
507
+ // Mark this chunk as processed.
508
+ resp_idx += 1;
509
+ }
510
+ })
511
+ .catch((e) => {
512
+ if (progressEvent.progress === 1) {
513
+ // Chunk may be incomplete, so only report errors when full response is available.
514
+ console.log("Error processing chunk", chunk, e);
515
+ }
516
+ return;
517
+ });
518
+ });
519
+ } catch (e) {
520
+ if (progressEvent.progress === 1) {
521
+ console.log("Error parsing chunk", chunk_json, e);
522
+ }
523
+ return;
524
+ }
517
525
  });
518
526
  };
519
527
 
@@ -537,7 +545,7 @@ export const uploadFiles = async (
537
545
  });
538
546
 
539
547
  // Send the file to the server.
540
- upload_controllers[upload_id] = controller;
548
+ refs[upload_ref_name] = controller;
541
549
 
542
550
  try {
543
551
  return await axios.post(getBackendURL(UPLOADURL), formdata, config);
@@ -557,7 +565,7 @@ export const uploadFiles = async (
557
565
  }
558
566
  return false;
559
567
  } finally {
560
- delete upload_controllers[upload_id];
568
+ delete refs[upload_ref_name];
561
569
  }
562
570
  };
563
571
 
@@ -799,7 +807,7 @@ export const useEventLoop = (
799
807
  connect(
800
808
  socket,
801
809
  dispatch,
802
- ["websocket", "polling"],
810
+ ["websocket"],
803
811
  setConnectErrors,
804
812
  client_storage
805
813
  );
reflex/__init__.py CHANGED
@@ -331,7 +331,7 @@ _MAPPING: dict = {
331
331
  "SessionStorage",
332
332
  ],
333
333
  "middleware": ["middleware", "Middleware"],
334
- "model": ["session", "Model"],
334
+ "model": ["asession", "session", "Model"],
335
335
  "state": [
336
336
  "var",
337
337
  "ComponentState",
reflex/__init__.pyi CHANGED
@@ -186,6 +186,7 @@ from .istate.wrappers import get_state as get_state
186
186
  from .middleware import Middleware as Middleware
187
187
  from .middleware import middleware as middleware
188
188
  from .model import Model as Model
189
+ from .model import asession as asession
189
190
  from .model import session as session
190
191
  from .page import page as page
191
192
  from .state import ComponentState as ComponentState
reflex/app.py CHANGED
@@ -17,6 +17,7 @@ import sys
17
17
  import traceback
18
18
  from datetime import datetime
19
19
  from pathlib import Path
20
+ from types import SimpleNamespace
20
21
  from typing import (
21
22
  TYPE_CHECKING,
22
23
  Any,
@@ -363,6 +364,11 @@ class App(MiddlewareMixin, LifespanMixin):
363
364
  max_http_buffer_size=constants.POLLING_MAX_HTTP_BUFFER_SIZE,
364
365
  ping_interval=constants.Ping.INTERVAL,
365
366
  ping_timeout=constants.Ping.TIMEOUT,
367
+ json=SimpleNamespace(
368
+ dumps=staticmethod(format.json_dumps),
369
+ loads=staticmethod(json.loads),
370
+ ),
371
+ transports=["websocket"],
366
372
  )
367
373
  elif getattr(self.sio, "async_mode", "") != "asgi":
368
374
  raise RuntimeError(
@@ -430,7 +436,7 @@ class App(MiddlewareMixin, LifespanMixin):
430
436
  allow_credentials=True,
431
437
  allow_methods=["*"],
432
438
  allow_headers=["*"],
433
- allow_origins=["*"],
439
+ allow_origins=get_config().cors_allowed_origins,
434
440
  )
435
441
 
436
442
  @property
@@ -467,7 +473,7 @@ class App(MiddlewareMixin, LifespanMixin):
467
473
 
468
474
  def add_page(
469
475
  self,
470
- component: Component | ComponentCallable,
476
+ component: Component | ComponentCallable | None = None,
471
477
  route: str | None = None,
472
478
  title: str | Var | None = None,
473
479
  description: str | Var | None = None,
@@ -490,17 +496,33 @@ class App(MiddlewareMixin, LifespanMixin):
490
496
  meta: The metadata of the page.
491
497
 
492
498
  Raises:
493
- ValueError: When the specified route name already exists.
499
+ PageValueError: When the component is not set for a non-404 page.
500
+ RouteValueError: When the specified route name already exists.
494
501
  """
495
502
  # If the route is not set, get it from the callable.
496
503
  if route is None:
497
504
  if not isinstance(component, Callable):
498
- raise ValueError("Route must be set if component is not a callable.")
505
+ raise exceptions.RouteValueError(
506
+ "Route must be set if component is not a callable."
507
+ )
499
508
  # Format the route.
500
509
  route = format.format_route(component.__name__)
501
510
  else:
502
511
  route = format.format_route(route, format_case=False)
503
512
 
513
+ if route == constants.Page404.SLUG:
514
+ if component is None:
515
+ component = Default404Page.create()
516
+ component = wait_for_client_redirect(self._generate_component(component))
517
+ title = title or constants.Page404.TITLE
518
+ description = description or constants.Page404.DESCRIPTION
519
+ image = image or constants.Page404.IMAGE
520
+ else:
521
+ if component is None:
522
+ raise exceptions.PageValueError(
523
+ "Component must be set for a non-404 page."
524
+ )
525
+
504
526
  # Check if the route given is valid
505
527
  verify_route_validity(route)
506
528
 
@@ -516,7 +538,7 @@ class App(MiddlewareMixin, LifespanMixin):
516
538
  if route == constants.PageNames.INDEX_ROUTE
517
539
  else f"`{route}`"
518
540
  )
519
- raise ValueError(
541
+ raise exceptions.RouteValueError(
520
542
  f"Duplicate page route {route_name} already exists. Make sure you do not have two"
521
543
  f" pages with the same route"
522
544
  )
@@ -633,10 +655,14 @@ class App(MiddlewareMixin, LifespanMixin):
633
655
  on_load: The event handler(s) that will be called each time the page load.
634
656
  meta: The metadata of the page.
635
657
  """
636
- if component is None:
637
- component = Default404Page.create()
658
+ console.deprecate(
659
+ feature_name="App.add_custom_404_page",
660
+ reason=f"Use app.add_page(component, route='/{constants.Page404.SLUG}') instead.",
661
+ deprecation_version="0.6.7",
662
+ removal_version="0.8.0",
663
+ )
638
664
  self.add_page(
639
- component=wait_for_client_redirect(self._generate_component(component)),
665
+ component=component,
640
666
  route=constants.Page404.SLUG,
641
667
  title=title or constants.Page404.TITLE,
642
668
  image=image or constants.Page404.IMAGE,
@@ -837,7 +863,7 @@ class App(MiddlewareMixin, LifespanMixin):
837
863
 
838
864
  # Render a default 404 page if the user didn't supply one
839
865
  if constants.Page404.SLUG not in self.unevaluated_pages:
840
- self.add_custom_404_page()
866
+ self.add_page(route=constants.Page404.SLUG)
841
867
 
842
868
  # Fix up the style.
843
869
  self.style = evaluate_style_namespaces(self.style)
@@ -947,12 +973,12 @@ class App(MiddlewareMixin, LifespanMixin):
947
973
  is not None
948
974
  ):
949
975
  executor = concurrent.futures.ProcessPoolExecutor(
950
- max_workers=number_of_processes,
976
+ max_workers=number_of_processes or None,
951
977
  mp_context=multiprocessing.get_context("fork"),
952
978
  )
953
979
  else:
954
980
  executor = concurrent.futures.ThreadPoolExecutor(
955
- max_workers=environment.REFLEX_COMPILE_THREADS.get()
981
+ max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
956
982
  )
957
983
 
958
984
  for route, component in zip(self.pages, page_components):
@@ -965,7 +991,6 @@ class App(MiddlewareMixin, LifespanMixin):
965
991
 
966
992
  def _submit_work(fn, *args, **kwargs):
967
993
  f = executor.submit(fn, *args, **kwargs)
968
- # f = executor.apipe(fn, *args, **kwargs)
969
994
  result_futures.append(f)
970
995
 
971
996
  # Compile the pre-compiled pages.
@@ -1157,7 +1182,7 @@ class App(MiddlewareMixin, LifespanMixin):
1157
1182
  if hasattr(handler_fn, "__name__"):
1158
1183
  _fn_name = handler_fn.__name__
1159
1184
  else:
1160
- _fn_name = handler_fn.__class__.__name__
1185
+ _fn_name = type(handler_fn).__name__
1161
1186
 
1162
1187
  if isinstance(handler_fn, functools.partial):
1163
1188
  raise ValueError(
@@ -1270,7 +1295,7 @@ async def process(
1270
1295
  await asyncio.create_task(
1271
1296
  app.event_namespace.emit(
1272
1297
  "reload",
1273
- data=format.json_dumps(event),
1298
+ data=event,
1274
1299
  to=sid,
1275
1300
  )
1276
1301
  )
@@ -1523,7 +1548,7 @@ class EventNamespace(AsyncNamespace):
1523
1548
  """
1524
1549
  # Creating a task prevents the update from being blocked behind other coroutines.
1525
1550
  await asyncio.create_task(
1526
- self.emit(str(constants.SocketEvent.EVENT), update.json(), to=sid)
1551
+ self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
1527
1552
  )
1528
1553
 
1529
1554
  async def on_event(self, sid, data):
@@ -1536,7 +1561,7 @@ class EventNamespace(AsyncNamespace):
1536
1561
  sid: The Socket.IO session id.
1537
1562
  data: The event data.
1538
1563
  """
1539
- fields = json.loads(data)
1564
+ fields = data
1540
1565
  # Get the event.
1541
1566
  event = Event(
1542
1567
  **{k: v for k, v in fields.items() if k not in ("handler", "event_actions")}
reflex/assets.py CHANGED
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  from typing import Optional
6
6
 
7
7
  from reflex import constants
8
- from reflex.utils.exec import is_backend_only
8
+ from reflex.config import EnvironmentVariables
9
9
 
10
10
 
11
11
  def asset(
@@ -52,7 +52,7 @@ def asset(
52
52
  The relative URL to the asset.
53
53
  """
54
54
  assets = constants.Dirs.APP_ASSETS
55
- backend_only = is_backend_only()
55
+ backend_only = EnvironmentVariables.REFLEX_BACKEND_ONLY.get()
56
56
 
57
57
  # Local asset handling
58
58
  if not shared:
reflex/base.py CHANGED
@@ -30,15 +30,16 @@ def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None
30
30
 
31
31
  # can't use reflex.config.environment here cause of circular import
32
32
  reload = os.getenv("__RELOAD_CONFIG", "").lower() == "true"
33
- for base in bases:
34
- try:
33
+ base = None
34
+ try:
35
+ for base in bases:
35
36
  if not reload and getattr(base, field_name, None):
36
37
  pass
37
- except TypeError as te:
38
- raise VarNameError(
39
- f'State var "{field_name}" in {base} has been shadowed by a substate var; '
40
- f'use a different field name instead".'
41
- ) from te
38
+ except TypeError as te:
39
+ raise VarNameError(
40
+ f'State var "{field_name}" in {base} has been shadowed by a substate var; '
41
+ f'use a different field name instead".'
42
+ ) from te
42
43
 
43
44
 
44
45
  # monkeypatch pydantic validate_field_name method to skip validating
@@ -45,6 +45,7 @@ class ReflexJinjaEnvironment(Environment):
45
45
  "on_load_internal": constants.CompileVars.ON_LOAD_INTERNAL,
46
46
  "update_vars_internal": constants.CompileVars.UPDATE_VARS_INTERNAL,
47
47
  "frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL,
48
+ "hook_position": constants.Hooks.HookPosition,
48
49
  }
49
50
 
50
51
 
reflex/compiler/utils.py CHANGED
@@ -115,7 +115,7 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]:
115
115
  default, rest = compile_import_statement(fields)
116
116
 
117
117
  # prevent lib from being rendered on the page if all imports are non rendered kind
118
- if not any({f.render for f in fields}): # type: ignore
118
+ if not any(f.render for f in fields): # type: ignore
119
119
  continue
120
120
 
121
121
  if not lib:
@@ -123,8 +123,7 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]:
123
123
  raise ValueError("No default field allowed for empty library.")
124
124
  if rest is None or len(rest) == 0:
125
125
  raise ValueError("No fields to import.")
126
- for module in sorted(rest):
127
- import_dicts.append(get_import_dict(module))
126
+ import_dicts.extend(get_import_dict(module) for module in sorted(rest))
128
127
  continue
129
128
 
130
129
  # remove the version before rendering the package imports
@@ -103,8 +103,8 @@ class Bare(Component):
103
103
  def _render(self) -> Tag:
104
104
  if isinstance(self.contents, Var):
105
105
  if isinstance(self.contents, (BooleanVar, ObjectVar)):
106
- return Tagless(contents=f"{{{str(self.contents.to_string())}}}")
107
- return Tagless(contents=f"{{{str(self.contents)}}}")
106
+ return Tagless(contents=f"{{{self.contents.to_string()!s}}}")
107
+ return Tagless(contents=f"{{{self.contents!s}}}")
108
108
  return Tagless(contents=str(self.contents))
109
109
 
110
110
  def _get_vars(self, include_children: bool = False) -> Iterator[Var]:
@@ -161,7 +161,7 @@ class ComponentNamespace(SimpleNamespace):
161
161
  Returns:
162
162
  The hash of the namespace.
163
163
  """
164
- return hash(self.__class__.__name__)
164
+ return hash(type(self).__name__)
165
165
 
166
166
 
167
167
  def evaluate_style_namespaces(style: ComponentStyle) -> dict:
@@ -583,7 +583,7 @@ class Component(BaseComponent, ABC):
583
583
  return self._create_event_chain(args_spec, value.guess_type(), key=key)
584
584
  else:
585
585
  raise ValueError(
586
- f"Invalid event chain: {str(value)} of type {value._var_type}"
586
+ f"Invalid event chain: {value!s} of type {value._var_type}"
587
587
  )
588
588
  elif isinstance(value, EventChain):
589
589
  # Trust that the caller knows what they're doing passing an EventChain directly
@@ -653,7 +653,6 @@ class Component(BaseComponent, ABC):
653
653
 
654
654
  Returns:
655
655
  The event triggers.
656
-
657
656
  """
658
657
  default_triggers: Dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]] = {
659
658
  EventTriggers.ON_FOCUS: no_args_event_spec,
@@ -1111,7 +1110,7 @@ class Component(BaseComponent, ABC):
1111
1110
  vars.append(prop_var)
1112
1111
 
1113
1112
  # Style keeps track of its own VarData instance, so embed in a temp Var that is yielded.
1114
- if isinstance(self.style, dict) and self.style or isinstance(self.style, Var):
1113
+ if (isinstance(self.style, dict) and self.style) or isinstance(self.style, Var):
1115
1114
  vars.append(
1116
1115
  Var(
1117
1116
  _js_expr="style",
@@ -1209,7 +1208,7 @@ class Component(BaseComponent, ABC):
1209
1208
  Yields:
1210
1209
  The parent classes that define the method (differently than the base).
1211
1210
  """
1212
- seen_methods = set([getattr(Component, method)])
1211
+ seen_methods = {getattr(Component, method)}
1213
1212
  for clz in cls.mro():
1214
1213
  if clz is Component:
1215
1214
  break
@@ -1369,7 +1368,9 @@ class Component(BaseComponent, ABC):
1369
1368
  if user_hooks_data is not None:
1370
1369
  other_imports.append(user_hooks_data.imports)
1371
1370
  other_imports.extend(
1372
- hook_imports for hook_imports in self._get_added_hooks().values()
1371
+ hook_vardata.imports
1372
+ for hook_vardata in self._get_added_hooks().values()
1373
+ if hook_vardata is not None
1373
1374
  )
1374
1375
 
1375
1376
  return imports.merge_imports(_imports, *other_imports)
@@ -1391,15 +1392,9 @@ class Component(BaseComponent, ABC):
1391
1392
 
1392
1393
  # Collect imports from Vars used directly by this component.
1393
1394
  var_datas = [var._get_all_var_data() for var in self._get_vars()]
1394
- var_imports: List[ImmutableParsedImportDict] = list(
1395
- map(
1396
- lambda var_data: var_data.imports,
1397
- filter(
1398
- None,
1399
- var_datas,
1400
- ),
1401
- )
1402
- )
1395
+ var_imports: List[ImmutableParsedImportDict] = [
1396
+ var_data.imports for var_data in var_datas if var_data is not None
1397
+ ]
1403
1398
 
1404
1399
  added_import_dicts: list[ParsedImportDict] = []
1405
1400
  for clz in self._iter_parent_classes_with_method("add_imports"):
@@ -1408,8 +1403,9 @@ class Component(BaseComponent, ABC):
1408
1403
  if not isinstance(list_of_import_dict, list):
1409
1404
  list_of_import_dict = [list_of_import_dict]
1410
1405
 
1411
- for import_dict in list_of_import_dict:
1412
- added_import_dicts.append(parse_imports(import_dict))
1406
+ added_import_dicts.extend(
1407
+ [parse_imports(import_dict) for import_dict in list_of_import_dict]
1408
+ )
1413
1409
 
1414
1410
  return imports.merge_imports(
1415
1411
  *self._get_props_imports(),
@@ -1466,7 +1462,9 @@ class Component(BaseComponent, ABC):
1466
1462
  """
1467
1463
  ref = self.get_ref()
1468
1464
  if ref is not None:
1469
- return f"const {ref} = useRef(null); {str(Var(_js_expr=ref)._as_ref())} = {ref};"
1465
+ return (
1466
+ f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};"
1467
+ )
1470
1468
 
1471
1469
  def _get_vars_hooks(self) -> dict[str, None]:
1472
1470
  """Get the hooks required by vars referenced in this component.
@@ -1521,7 +1519,7 @@ class Component(BaseComponent, ABC):
1521
1519
  **self._get_special_hooks(),
1522
1520
  }
1523
1521
 
1524
- def _get_added_hooks(self) -> dict[str, ImportDict]:
1522
+ def _get_added_hooks(self) -> dict[str, VarData | None]:
1525
1523
  """Get the hooks added via `add_hooks` method.
1526
1524
 
1527
1525
  Returns:
@@ -1530,17 +1528,15 @@ class Component(BaseComponent, ABC):
1530
1528
  code = {}
1531
1529
 
1532
1530
  def extract_var_hooks(hook: Var):
1533
- _imports = {}
1534
1531
  var_data = VarData.merge(hook._get_all_var_data())
1535
1532
  if var_data is not None:
1536
1533
  for sub_hook in var_data.hooks:
1537
- code[sub_hook] = {}
1538
- if var_data.imports:
1539
- _imports = var_data.imports
1534
+ code[sub_hook] = None
1535
+
1540
1536
  if str(hook) in code:
1541
- code[str(hook)] = imports.merge_imports(code[str(hook)], _imports)
1537
+ code[str(hook)] = VarData.merge(var_data, code[str(hook)])
1542
1538
  else:
1543
- code[str(hook)] = _imports
1539
+ code[str(hook)] = var_data
1544
1540
 
1545
1541
  # Add the hook code from add_hooks for each parent class (this is reversed to preserve
1546
1542
  # the order of the hooks in the final output)
@@ -1549,7 +1545,7 @@ class Component(BaseComponent, ABC):
1549
1545
  if isinstance(hook, Var):
1550
1546
  extract_var_hooks(hook)
1551
1547
  else:
1552
- code[hook] = {}
1548
+ code[hook] = None
1553
1549
 
1554
1550
  return code
1555
1551
 
@@ -1591,8 +1587,7 @@ class Component(BaseComponent, ABC):
1591
1587
  if hooks is not None:
1592
1588
  code[hooks] = None
1593
1589
 
1594
- for hook in self._get_added_hooks():
1595
- code[hook] = None
1590
+ code.update(self._get_added_hooks())
1596
1591
 
1597
1592
  # Add the hook code for the children.
1598
1593
  for child in self.children:
@@ -2194,6 +2189,31 @@ class StatefulComponent(BaseComponent):
2194
2189
  ]
2195
2190
  return [var_name]
2196
2191
 
2192
+ @staticmethod
2193
+ def _get_deps_from_event_trigger(event: EventChain | EventSpec | Var) -> set[str]:
2194
+ """Get the dependencies accessed by event triggers.
2195
+
2196
+ Args:
2197
+ event: The event trigger to extract deps from.
2198
+
2199
+ Returns:
2200
+ The dependencies accessed by the event triggers.
2201
+ """
2202
+ events: list = [event]
2203
+ deps = set()
2204
+
2205
+ if isinstance(event, EventChain):
2206
+ events.extend(event.events)
2207
+
2208
+ for ev in events:
2209
+ if isinstance(ev, EventSpec):
2210
+ for arg in ev.args:
2211
+ for a in arg:
2212
+ var_datas = VarData.merge(a._get_all_var_data())
2213
+ if var_datas and var_datas.deps is not None:
2214
+ deps |= {str(dep) for dep in var_datas.deps}
2215
+ return deps
2216
+
2197
2217
  @classmethod
2198
2218
  def _get_memoized_event_triggers(
2199
2219
  cls,
@@ -2230,6 +2250,11 @@ class StatefulComponent(BaseComponent):
2230
2250
 
2231
2251
  # Calculate Var dependencies accessed by the handler for useCallback dep array.
2232
2252
  var_deps = ["addEvents", "Event"]
2253
+
2254
+ # Get deps from event trigger var data.
2255
+ var_deps.extend(cls._get_deps_from_event_trigger(event))
2256
+
2257
+ # Get deps from hooks.
2233
2258
  for arg in event_args:
2234
2259
  var_data = arg._get_all_var_data()
2235
2260
  if var_data is None:
@@ -2563,7 +2588,7 @@ class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar):
2563
2588
  Returns:
2564
2589
  The hash of the var.
2565
2590
  """
2566
- return hash((self.__class__.__name__, self._js_expr))
2591
+ return hash((type(self).__name__, self._js_expr))
2567
2592
 
2568
2593
  @classmethod
2569
2594
  def create(
@@ -109,7 +109,7 @@ class ConnectionToaster(Toaster):
109
109
  )
110
110
 
111
111
  individual_hooks = [
112
- f"const toast_props = {str(LiteralVar.create(props))};",
112
+ f"const toast_props = {LiteralVar.create(props)!s};",
113
113
  "const [userDismissed, setUserDismissed] = useState(false);",
114
114
  FunctionStringVar(
115
115
  "useEffect",
@@ -124,7 +124,7 @@ class ConnectionToaster(Toaster):
124
124
  Var(
125
125
  _js_expr=f"""
126
126
  () => {{
127
- if ({str(has_too_many_connection_errors)}) {{
127
+ if ({has_too_many_connection_errors!s}) {{
128
128
  if (!userDismissed) {{
129
129
  toast.error(
130
130
  `Cannot connect to server: ${{{connection_error}}}.`,
@@ -321,7 +321,7 @@ class ConnectionPulser(Div):
321
321
  """Create a connection pulser component.
322
322
 
323
323
  Args:
324
- access_key: Provides a hint for generating a keyboard shortcut for the current element.
324
+ access_key: Provides a hint for generating a keyboard shortcut for the current element.
325
325
  auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
326
326
  content_editable: Indicates whether the element's content is editable.
327
327
  context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
@@ -24,7 +24,7 @@ class ClientSideRouting(Component):
24
24
  library = "$/utils/client_side_routing"
25
25
  tag = "useClientSideRouting"
26
26
 
27
- def add_hooks(self) -> list[str]:
27
+ def add_hooks(self) -> list[str | Var]:
28
28
  """Get the hooks to render.
29
29
 
30
30
  Returns:
@@ -66,4 +66,4 @@ class Default404Page(Component):
66
66
  tag = "Error"
67
67
  is_default = True
68
68
 
69
- status_code: Var[int] = 404 # type: ignore
69
+ status_code: Var[int] = Var.create(404)
@@ -13,7 +13,7 @@ from reflex.vars.base import Var
13
13
  route_not_found: Var
14
14
 
15
15
  class ClientSideRouting(Component):
16
- def add_hooks(self) -> list[str]: ...
16
+ def add_hooks(self) -> list[str | Var]: ...
17
17
  def render(self) -> str: ...
18
18
  @overload
19
19
  @classmethod