diff --git a/reflex/app.py b/reflex/app.py
index 65cb5bfdf..6e66257b4 100644
--- a/reflex/app.py
+++ b/reflex/app.py
@@ -75,6 +75,7 @@ from reflex.components.core.client_side_routing import (
from reflex.components.core.sticky import sticky
from reflex.components.core.upload import Upload, get_upload_dir
from reflex.components.radix import themes
+from reflex.components.sonner.toast import toast
from reflex.config import ExecutorType, environment, get_config
from reflex.event import (
_EVENT_FIELDS,
@@ -84,7 +85,6 @@ from reflex.event import (
EventType,
IndividualEventType,
get_hydrate_event,
- window_alert,
)
from reflex.model import Model, get_db_status
from reflex.page import DECORATED_PAGES
@@ -144,7 +144,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
EventSpec: The window alert event.
"""
- from reflex.components.sonner.toast import Toaster, toast
+ from reflex.components.sonner.toast import toast
error = traceback.format_exc()
@@ -155,18 +155,16 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
if is_prod_mode()
else [f"{type(exception).__name__}: {exception}.", "See logs for details."]
)
- if Toaster.is_used:
- return toast(
- "An error occurred.",
- level="error",
- description="
".join(error_message),
- position="top-center",
- id="backend_error",
- style={"width": "500px"},
- )
- else:
- error_message.insert(0, "An error occurred.")
- return window_alert("\n".join(error_message))
+
+ return toast(
+ "An error occurred.",
+ level="error",
+ fallback_to_alert=True,
+ description="
".join(error_message),
+ position="top-center",
+ id="backend_error",
+ style={"width": "500px"},
+ )
def extra_overlay_function() -> Optional[Component]:
@@ -414,7 +412,7 @@ class App(MiddlewareMixin, LifespanMixin):
] = default_backend_exception_handler
# Put the toast provider in the app wrap.
- bundle_toaster: bool = True
+ toaster: Component | None = dataclasses.field(default_factory=toast.provider)
@property
def api(self) -> FastAPI | None:
@@ -1100,10 +1098,6 @@ class App(MiddlewareMixin, LifespanMixin):
should_compile = self._should_compile()
if not should_compile:
- if self.bundle_toaster:
- from reflex.components.sonner.toast import Toaster
-
- Toaster.is_used = True
with console.timing("Evaluate Pages (Backend)"):
for route in self._unevaluated_pages:
console.debug(f"Evaluating page: {route}")
@@ -1133,20 +1127,6 @@ class App(MiddlewareMixin, LifespanMixin):
+ adhoc_steps_without_executor,
)
- if self.bundle_toaster:
- from reflex.components.component import memo
- from reflex.components.sonner.toast import toast
-
- internal_toast_provider = toast.provider()
-
- @memo
- def memoized_toast_provider():
- return internal_toast_provider
-
- toast_provider = Fragment.create(memoized_toast_provider())
-
- app_wrappers[(1, "ToasterProvider")] = toast_provider
-
with console.timing("Evaluate Pages (Frontend)"):
performance_metrics: list[tuple[str, float]] = []
for route in self._unevaluated_pages:
@@ -1207,6 +1187,17 @@ class App(MiddlewareMixin, LifespanMixin):
# Add the custom components from the page to the set.
custom_components |= component._get_all_custom_components()
+ if (toaster := self.toaster) is not None:
+ from reflex.components.component import memo
+
+ @memo
+ def memoized_toast_provider():
+ return toaster
+
+ toast_provider = Fragment.create(memoized_toast_provider())
+
+ app_wrappers[(1, "ToasterProvider")] = toast_provider
+
# Add the app wraps to the app.
for key, app_wrap in self.app_wraps.items():
component = app_wrap(self._state is not None)
diff --git a/reflex/components/core/banner.py b/reflex/components/core/banner.py
index 1fc631616..c0e88b23e 100644
--- a/reflex/components/core/banner.py
+++ b/reflex/components/core/banner.py
@@ -5,6 +5,7 @@ from __future__ import annotations
from typing import Optional
from reflex import constants
+from reflex.components.base.fragment import Fragment
from reflex.components.component import Component
from reflex.components.core.cond import cond
from reflex.components.el.elements.typography import Div
@@ -16,7 +17,7 @@ from reflex.components.radix.themes.components.dialog import (
)
from reflex.components.radix.themes.layout.flex import Flex
from reflex.components.radix.themes.typography.text import Text
-from reflex.components.sonner.toast import Toaster, ToastProps
+from reflex.components.sonner.toast import ToastProps, toast_ref
from reflex.config import environment
from reflex.constants import Dirs, Hooks, Imports
from reflex.constants.compiler import CompileVars
@@ -90,7 +91,7 @@ def default_connection_error() -> list[str | Var | Component]:
]
-class ConnectionToaster(Toaster):
+class ConnectionToaster(Fragment):
"""A connection toaster component."""
def add_hooks(self) -> list[str | Var]:
@@ -113,11 +114,11 @@ class ConnectionToaster(Toaster):
if environment.REFLEX_DOES_BACKEND_COLD_START.get():
loading_message = Var.create("Backend is starting.")
backend_is_loading_toast_var = Var(
- f"toast.loading({loading_message!s}, {{...toast_props, description: '', closeButton: false, onDismiss: () => setUserDismissed(true)}},)"
+ f"toast?.loading({loading_message!s}, {{...toast_props, description: '', closeButton: false, onDismiss: () => setUserDismissed(true)}},)"
)
backend_is_not_responding = Var.create("Backend is not responding.")
backend_is_down_toast_var = Var(
- f"toast.error({backend_is_not_responding!s}, {{...toast_props, description: '', onDismiss: () => setUserDismissed(true)}},)"
+ f"toast?.error({backend_is_not_responding!s}, {{...toast_props, description: '', onDismiss: () => setUserDismissed(true)}},)"
)
toast_var = Var(
f"""
@@ -138,10 +139,11 @@ setTimeout(() => {{
f"Cannot connect to server: {connection_error}."
)
toast_var = Var(
- f"toast.error({loading_message!s}, {{...toast_props, onDismiss: () => setUserDismissed(true)}},)"
+ f"toast?.error({loading_message!s}, {{...toast_props, onDismiss: () => setUserDismissed(true)}},)"
)
individual_hooks = [
+ Var(f"const toast = {toast_ref};"),
f"const toast_props = {LiteralVar.create(props)!s};",
"const [userDismissed, setUserDismissed] = useState(false);",
"const [waitedForBackend, setWaitedForBackend] = useState(false);",
@@ -163,7 +165,7 @@ setTimeout(() => {{
{toast_var!s}
}}
}} else {{
- toast.dismiss("{toast_id}");
+ toast?.dismiss("{toast_id}");
setUserDismissed(false); // after reconnection reset dismissed state
}}
}}
@@ -189,7 +191,6 @@ setTimeout(() => {{
Returns:
The connection toaster component.
"""
- Toaster.is_used = True
return super().create(*children, **props)
diff --git a/reflex/components/core/banner.pyi b/reflex/components/core/banner.pyi
index ce299f443..52fc65787 100644
--- a/reflex/components/core/banner.pyi
+++ b/reflex/components/core/banner.pyi
@@ -5,10 +5,10 @@
# ------------------------------------------------------
from typing import Any, Dict, Literal, Optional, Union, overload
+from reflex.components.base.fragment import Fragment
from reflex.components.component import Component
from reflex.components.el.elements.typography import Div
from reflex.components.lucide.icon import Icon
-from reflex.components.sonner.toast import Toaster, ToastProps
from reflex.constants.compiler import CompileVars
from reflex.event import EventType
from reflex.style import Style
@@ -41,48 +41,13 @@ class WebsocketTargetURL(Var):
def default_connection_error() -> list[str | Var | Component]: ...
-class ConnectionToaster(Toaster):
+class ConnectionToaster(Fragment):
def add_hooks(self) -> list[str | Var]: ...
@overload
@classmethod
def create( # type: ignore
cls,
*children,
- theme: Optional[Union[Var[str], str]] = None,
- rich_colors: Optional[Union[Var[bool], bool]] = None,
- expand: Optional[Union[Var[bool], bool]] = None,
- visible_toasts: Optional[Union[Var[int], int]] = None,
- position: Optional[
- Union[
- Literal[
- "bottom-center",
- "bottom-left",
- "bottom-right",
- "top-center",
- "top-left",
- "top-right",
- ],
- Var[
- Literal[
- "bottom-center",
- "bottom-left",
- "bottom-right",
- "top-center",
- "top-left",
- "top-right",
- ]
- ],
- ]
- ] = None,
- close_button: Optional[Union[Var[bool], bool]] = None,
- offset: Optional[Union[Var[str], str]] = None,
- dir: Optional[Union[Var[str], str]] = None,
- hotkey: Optional[Union[Var[str], str]] = None,
- invert: Optional[Union[Var[bool], bool]] = None,
- toast_options: Optional[Union[ToastProps, Var[ToastProps]]] = None,
- gap: Optional[Union[Var[int], int]] = None,
- loading_icon: Optional[Union[Icon, Var[Icon]]] = None,
- pause_when_page_is_hidden: Optional[Union[Var[bool], bool]] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
id: Optional[Any] = None,
@@ -110,20 +75,6 @@ class ConnectionToaster(Toaster):
Args:
*children: The children of the component.
- theme: the theme of the toast
- rich_colors: whether to show rich colors
- expand: whether to expand the toast
- visible_toasts: the number of toasts that are currently visible
- position: the position of the toast
- close_button: whether to show the close button
- offset: offset of the toast
- dir: directionality of the toast (default: ltr)
- hotkey: Keyboard shortcut that will move focus to the toaster area.
- invert: Dark toasts in light mode and vice versa.
- toast_options: These will act as default options for all toasts. See toast() for all available options.
- gap: Gap between toasts when expanded
- loading_icon: Changes the default loading icon
- pause_when_page_is_hidden: Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
style: The style of the component.
key: A unique key for the component.
id: The id for the component.
diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py
index d1f9464d8..ce52a9ce6 100644
--- a/reflex/components/sonner/toast.py
+++ b/reflex/components/sonner/toast.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Any, ClassVar, Literal, Optional, Union
+from typing import Any, Literal, Optional, Union
from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
@@ -17,6 +17,7 @@ from reflex.utils.serializers import serializer
from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var
from reflex.vars.function import FunctionVar
+from reflex.vars.number import ternary_operation
from reflex.vars.object import ObjectVar
LiteralPosition = Literal[
@@ -217,9 +218,6 @@ class Toaster(Component):
# Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
pause_when_page_is_hidden: Var[bool]
- # Marked True when any Toast component is created.
- is_used: ClassVar[bool] = False
-
def add_hooks(self) -> list[Var | str]:
"""Add hooks for the toaster component.
@@ -241,13 +239,17 @@ class Toaster(Component):
@staticmethod
def send_toast(
- message: str | Var = "", level: str | None = None, **props
+ message: str | Var = "",
+ level: str | None = None,
+ fallback_to_alert: bool = False,
+ **props,
) -> EventSpec:
"""Send a toast message.
Args:
message: The message to display.
level: The level of the toast.
+ fallback_to_alert: Whether to fallback to an alert if the toaster is not created.
**props: The options for the toast.
Raises:
@@ -256,11 +258,6 @@ class Toaster(Component):
Returns:
The toast event.
"""
- if not Toaster.is_used:
- raise ValueError(
- "Toaster component must be created before sending a toast. (use `rx.toast.provider()`)"
- )
-
toast_command = (
ObjectVar.__getattr__(toast_ref.to(dict), level) if level else toast_ref
).to(FunctionVar)
@@ -277,6 +274,21 @@ class Toaster(Component):
else:
toast = toast_command.call(message)
+ if fallback_to_alert:
+ toast = ternary_operation(
+ toast_ref.bool(),
+ toast,
+ FunctionVar("window.alert").call(
+ Var.create(
+ message
+ if isinstance(message, str) and message
+ else props.get("title", props.get("description", ""))
+ )
+ .to(str)
+ .replace("
", "\n")
+ ),
+ )
+
return run_script(toast)
@staticmethod
@@ -379,7 +391,6 @@ class Toaster(Component):
Returns:
The toaster component.
"""
- cls.is_used = True
return super().create(*children, **props)
diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi
index cb637bfff..220e5b6b0 100644
--- a/reflex/components/sonner/toast.pyi
+++ b/reflex/components/sonner/toast.pyi
@@ -3,7 +3,7 @@
# ------------------- DO NOT EDIT ----------------------
# This file was generated by `reflex/utils/pyi_generator.py`!
# ------------------------------------------------------
-from typing import Any, ClassVar, Dict, Literal, Optional, Union, overload
+from typing import Any, Dict, Literal, Optional, Union, overload
from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
@@ -60,12 +60,13 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
class Toaster(Component):
- is_used: ClassVar[bool] = False
-
def add_hooks(self) -> list[Var | str]: ...
@staticmethod
def send_toast(
- message: str | Var = "", level: str | None = None, **props
+ message: str | Var = "",
+ level: str | None = None,
+ fallback_to_alert: bool = False,
+ **props,
) -> EventSpec: ...
@staticmethod
def toast_info(message: str | Var = "", **kwargs: Any): ...
@@ -185,13 +186,17 @@ class ToastNamespace(ComponentNamespace):
@staticmethod
def __call__(
- message: Union[str, Var] = "", level: Optional[str] = None, **props
+ message: Union[str, Var] = "",
+ level: Optional[str] = None,
+ fallback_to_alert: bool = False,
+ **props,
) -> "EventSpec":
"""Send a toast message.
Args:
message: The message to display.
level: The level of the toast.
+ fallback_to_alert: Whether to fallback to an alert if the toaster is not created.
**props: The options for the toast.
Raises:
diff --git a/tests/units/test_state.py b/tests/units/test_state.py
index e0390c5ac..fc4b5fe3b 100644
--- a/tests/units/test_state.py
+++ b/tests/units/test_state.py
@@ -35,7 +35,6 @@ import reflex.config
from reflex import constants
from reflex.app import App
from reflex.base import Base
-from reflex.components.sonner.toast import Toaster
from reflex.constants import CompileVars, RouteVar, SocketEvent
from reflex.event import Event, EventHandler
from reflex.state import (
@@ -1613,29 +1612,20 @@ async def test_state_with_invalid_yield(capsys, mock_app):
rx.event.Event(token="fake_token", name="invalid_handler")
):
assert not update.delta
- if Toaster.is_used:
- assert update.events == rx.event.fix_events(
- [
- rx.toast(
- "An error occurred.",
- description="TypeError: Your handler test_state_with_invalid_yield..StateWithInvalidYield.invalid_handler must only return/yield: None, Events or other EventHandlers referenced by their class (not using `self`).
See logs for details.",
- level="error",
- id="backend_error",
- position="top-center",
- style={"width": "500px"},
- )
- ],
- token="",
- )
- else:
- assert update.events == rx.event.fix_events(
- [
- rx.window_alert(
- "An error occurred.\nContact the website administrator."
- )
- ],
- token="",
- )
+ assert update.events == rx.event.fix_events(
+ [
+ rx.toast(
+ "An error occurred.",
+ level="error",
+ fallback_to_alert=True,
+ description="TypeError: Your handler test_state_with_invalid_yield..StateWithInvalidYield.invalid_handler must only return/yield: None, Events or other EventHandlers referenced by their class (not using `self`).
See logs for details.",
+ id="backend_error",
+ position="top-center",
+ style={"width": "500px"},
+ )
+ ],
+ token="",
+ )
captured = capsys.readouterr()
assert "must only return/yield: None, Events or other EventHandlers" in captured.out