client_state.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. """Handle client side state with `useState`."""
  2. from __future__ import annotations
  3. import dataclasses
  4. import sys
  5. from typing import Any, Callable, Optional, Type, Union
  6. from reflex import constants
  7. from reflex.event import EventChain, EventHandler, EventSpec, call_script
  8. from reflex.utils.imports import ImportVar
  9. from reflex.vars import Var, VarData
  10. NoValue = object()
  11. _refs_import = {
  12. f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="refs")],
  13. }
  14. def _client_state_ref(var_name: str) -> str:
  15. """Get the ref path for a ClientStateVar.
  16. Args:
  17. var_name: The name of the variable.
  18. Returns:
  19. An accessor for ClientStateVar ref as a string.
  20. """
  21. return f"refs['_client_state_{var_name}']"
  22. @dataclasses.dataclass(
  23. eq=False,
  24. **{"slots": True} if sys.version_info >= (3, 10) else {},
  25. )
  26. class ClientStateVar(Var):
  27. """A Var that exists on the client via useState."""
  28. # The name of the var.
  29. _var_name: str = dataclasses.field()
  30. # Track the names of the getters and setters
  31. _setter_name: str = dataclasses.field()
  32. _getter_name: str = dataclasses.field()
  33. # Whether to add the var and setter to the global `refs` object for use in any Component.
  34. _global_ref: bool = dataclasses.field(default=True)
  35. # The type of the var.
  36. _var_type: Type = dataclasses.field(default=Any)
  37. # Whether this is a local javascript variable.
  38. _var_is_local: bool = dataclasses.field(default=False)
  39. # Whether the var is a string literal.
  40. _var_is_string: bool = dataclasses.field(default=False)
  41. # _var_full_name should be prefixed with _var_state
  42. _var_full_name_needs_state_prefix: bool = dataclasses.field(default=False)
  43. # Extra metadata associated with the Var
  44. _var_data: Optional[VarData] = dataclasses.field(default=None)
  45. def __hash__(self) -> int:
  46. """Define a hash function for a var.
  47. Returns:
  48. The hash of the var.
  49. """
  50. return hash(
  51. (self._var_name, str(self._var_type), self._getter_name, self._setter_name)
  52. )
  53. @classmethod
  54. def create(
  55. cls, var_name: str, default: Any = NoValue, global_ref: bool = True
  56. ) -> "ClientStateVar":
  57. """Create a local_state Var that can be accessed and updated on the client.
  58. The `ClientStateVar` should be included in the highest parent component
  59. that contains the components which will access and manipulate the client
  60. state. It has no visual rendering, including it ensures that the
  61. `useState` hook is called in the correct scope.
  62. To render the var in a component, use the `value` property.
  63. To update the var in a component, use the `set` property or `set_value` method.
  64. To access the var in an event handler, use the `retrieve` method with
  65. `callback` set to the event handler which should receive the value.
  66. To update the var in an event handler, use the `push` method with the
  67. value to update.
  68. Args:
  69. var_name: The name of the variable.
  70. default: The default value of the variable.
  71. global_ref: Whether the state should be accessible in any Component and on the backend.
  72. Returns:
  73. ClientStateVar
  74. """
  75. assert isinstance(var_name, str), "var_name must be a string."
  76. if default is NoValue:
  77. default_var = Var.create_safe("", _var_is_local=False, _var_is_string=False)
  78. elif not isinstance(default, Var):
  79. default_var = Var.create_safe(
  80. default,
  81. _var_is_string=isinstance(default, str),
  82. )
  83. else:
  84. default_var = default
  85. setter_name = f"set{var_name.capitalize()}"
  86. hooks = {
  87. f"const [{var_name}, {setter_name}] = useState({default_var._var_name_unwrapped})": None,
  88. }
  89. imports = {
  90. "react": [ImportVar(tag="useState")],
  91. }
  92. if global_ref:
  93. hooks[f"{_client_state_ref(var_name)} = {var_name}"] = None
  94. hooks[f"{_client_state_ref(setter_name)} = {setter_name}"] = None
  95. imports.update(_refs_import)
  96. return cls(
  97. _var_name="",
  98. _setter_name=setter_name,
  99. _getter_name=var_name,
  100. _global_ref=global_ref,
  101. _var_is_local=False,
  102. _var_is_string=False,
  103. _var_type=default_var._var_type,
  104. _var_data=VarData.merge(
  105. default_var._var_data,
  106. VarData( # type: ignore
  107. hooks=hooks,
  108. imports=imports,
  109. ),
  110. ),
  111. )
  112. @property
  113. def value(self) -> Var:
  114. """Get a placeholder for the Var.
  115. This property can only be rendered on the frontend.
  116. To access the value in a backend event handler, see `retrieve`.
  117. Returns:
  118. an accessor for the client state variable.
  119. """
  120. return (
  121. Var.create_safe(
  122. _client_state_ref(self._getter_name)
  123. if self._global_ref
  124. else self._getter_name,
  125. _var_is_local=False,
  126. _var_is_string=False,
  127. )
  128. .to(self._var_type)
  129. ._replace(
  130. merge_var_data=VarData( # type: ignore
  131. imports=_refs_import if self._global_ref else {}
  132. )
  133. )
  134. )
  135. def set_value(self, value: Any = NoValue) -> Var:
  136. """Set the value of the client state variable.
  137. This property can only be attached to a frontend event trigger.
  138. To set a value from a backend event handler, see `push`.
  139. Args:
  140. value: The value to set.
  141. Returns:
  142. A special EventChain Var which will set the value when triggered.
  143. """
  144. setter = (
  145. _client_state_ref(self._setter_name)
  146. if self._global_ref
  147. else self._setter_name
  148. )
  149. if value is not NoValue:
  150. # This is a hack to make it work like an EventSpec taking an arg
  151. value = Var.create_safe(value, _var_is_string=isinstance(value, str))
  152. if not value._var_is_string and value._var_full_name.startswith("_"):
  153. arg = value._var_name_unwrapped
  154. else:
  155. arg = ""
  156. setter = f"({arg}) => {setter}({value._var_name_unwrapped})"
  157. return (
  158. Var.create_safe(
  159. setter,
  160. _var_is_local=False,
  161. _var_is_string=False,
  162. )
  163. .to(EventChain)
  164. ._replace(
  165. merge_var_data=VarData( # type: ignore
  166. imports=_refs_import if self._global_ref else {}
  167. )
  168. )
  169. )
  170. @property
  171. def set(self) -> Var:
  172. """Set the value of the client state variable.
  173. This property can only be attached to a frontend event trigger.
  174. To set a value from a backend event handler, see `push`.
  175. Returns:
  176. A special EventChain Var which will set the value when triggered.
  177. """
  178. return self.set_value()
  179. def retrieve(
  180. self, callback: Union[EventHandler, Callable, None] = None
  181. ) -> EventSpec:
  182. """Pass the value of the client state variable to a backend EventHandler.
  183. The event handler must `yield` or `return` the EventSpec to trigger the event.
  184. Args:
  185. callback: The callback to pass the value to.
  186. Returns:
  187. An EventSpec which will retrieve the value when triggered.
  188. Raises:
  189. ValueError: If the ClientStateVar is not global.
  190. """
  191. if not self._global_ref:
  192. raise ValueError("ClientStateVar must be global to retrieve the value.")
  193. return call_script(_client_state_ref(self._getter_name), callback=callback)
  194. def push(self, value: Any) -> EventSpec:
  195. """Push a value to the client state variable from the backend.
  196. The event handler must `yield` or `return` the EventSpec to trigger the event.
  197. Args:
  198. value: The value to update.
  199. Returns:
  200. An EventSpec which will push the value when triggered.
  201. Raises:
  202. ValueError: If the ClientStateVar is not global.
  203. """
  204. if not self._global_ref:
  205. raise ValueError("ClientStateVar must be global to push the value.")
  206. return call_script(f"{_client_state_ref(self._setter_name)}({value})")