diff --git a/reflex/app.py b/reflex/app.py
index 658ba1a1f..7e40a95bf 100644
--- a/reflex/app.py
+++ b/reflex/app.py
@@ -112,11 +112,29 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
EventSpec: The window alert event.
"""
+ from reflex.components.sonner.toast import Toaster, toast
+
error = traceback.format_exc()
console.error(f"[Reflex Backend Exception]\n {error}\n")
- return window_alert("An error occurred. See logs for details.")
+ error_message = (
+ ["Contact the website administrator."]
+ if is_prod_mode()
+ else [f"{type(exception).__name__}: {exception}.", "See logs for details."]
+ )
+ if Toaster.is_used:
+ return toast(
+ level="error",
+ title="An error occurred.",
+ description="
".join(error_message),
+ position="top-center",
+ id="backend_error",
+ style={"width": "500px"},
+ ) # type: ignore
+ else:
+ error_message.insert(0, "An error occurred.")
+ return window_alert("\n".join(error_message))
def default_overlay_component() -> Component:
@@ -183,7 +201,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
# A component that is present on every page (defaults to the Connection Error banner).
overlay_component: Optional[Union[Component, ComponentCallable]] = (
- default_overlay_component
+ default_overlay_component()
)
# Error boundary component to wrap the app with.
diff --git a/reflex/components/core/banner.py b/reflex/components/core/banner.py
index b634ab75a..c6b46696c 100644
--- a/reflex/components/core/banner.py
+++ b/reflex/components/core/banner.py
@@ -153,6 +153,20 @@ useEffect(() => {{
hook,
]
+ @classmethod
+ def create(cls, *children, **props) -> Component:
+ """Create a connection toaster component.
+
+ Args:
+ *children: The children of the component.
+ **props: The properties of the component.
+
+ Returns:
+ The connection toaster component.
+ """
+ Toaster.is_used = True
+ return super().create(*children, **props)
+
class ConnectionBanner(Component):
"""A connection banner component."""
diff --git a/reflex/components/core/banner.pyi b/reflex/components/core/banner.pyi
index ddaafb153..b9b6d506f 100644
--- a/reflex/components/core/banner.pyi
+++ b/reflex/components/core/banner.pyi
@@ -187,7 +187,7 @@ class ConnectionToaster(Toaster):
] = None,
**props,
) -> "ConnectionToaster":
- """Create the component.
+ """Create a connection toaster component.
Args:
*children: The children of the component.
@@ -211,10 +211,10 @@ class ConnectionToaster(Toaster):
class_name: The class name for the component.
autofocus: Whether the component should take the focus once the page is loaded
custom_attrs: custom attribute
- **props: The props of the component.
+ **props: The properties of the component.
Returns:
- The component.
+ The connection toaster component.
"""
...
diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py
index f8d1cc340..d4df31e82 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, Literal, Optional, Union
+from typing import Any, ClassVar, Literal, Optional, Union
from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
@@ -211,6 +211,9 @@ 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.
@@ -231,7 +234,7 @@ class Toaster(Component):
return [hook]
@staticmethod
- def send_toast(message: str, level: str | None = None, **props) -> EventSpec:
+ def send_toast(message: str = "", level: str | None = None, **props) -> EventSpec:
"""Send a toast message.
Args:
@@ -239,10 +242,19 @@ class Toaster(Component):
level: The level of the toast.
**props: The options for the toast.
+ Raises:
+ ValueError: If the Toaster component is not created.
+
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 = f"{toast_ref}.{level}" if level is not None else toast_ref
+ if message == "" and ("title" not in props or "description" not in props):
+ raise ValueError("Toast message or title or description must be provided.")
if props:
args = serialize(ToastProps(**props)) # type: ignore
toast = f"{toast_command}(`{message}`, {args})"
@@ -331,6 +343,20 @@ class Toaster(Component):
)
return call_script(dismiss_action)
+ @classmethod
+ def create(cls, *children, **props) -> Component:
+ """Create a toaster component.
+
+ Args:
+ *children: The children of the toaster.
+ **props: The properties of the toaster.
+
+ Returns:
+ The toaster component.
+ """
+ cls.is_used = True
+ return super().create(*children, **props)
+
# TODO: figure out why loading toast stay open forever
# def toast_loading(message: str, **kwargs):
diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi
index 82999a06a..7e5758b16 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, Callable, Dict, Literal, Optional, Union, overload
+from typing import Any, Callable, ClassVar, Dict, Literal, Optional, Union, overload
from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
@@ -52,9 +52,13 @@ class ToastProps(PropsBase):
def dict(self, *args, **kwargs) -> dict[str, Any]: ...
class Toaster(Component):
+ is_used: ClassVar[bool] = False
+
def add_hooks(self) -> list[Var | str]: ...
@staticmethod
- def send_toast(message: str, level: str | None = None, **props) -> EventSpec: ...
+ def send_toast(
+ message: str = "", level: str | None = None, **props
+ ) -> EventSpec: ...
@staticmethod
def toast_info(message: str, **kwargs): ...
@staticmethod
@@ -158,10 +162,10 @@ class Toaster(Component):
] = None,
**props,
) -> "Toaster":
- """Create the component.
+ """Create a toaster component.
Args:
- *children: The children of the component.
+ *children: The children of the toaster.
theme: the theme of the toast
rich_colors: whether to show rich colors
expand: whether to expand the toast
@@ -182,10 +186,10 @@ class Toaster(Component):
class_name: The class name for the component.
autofocus: Whether the component should take the focus once the page is loaded
custom_attrs: custom attribute
- **props: The props of the component.
+ **props: The properties of the toaster.
Returns:
- The component.
+ The toaster component.
"""
...
@@ -200,7 +204,7 @@ class ToastNamespace(ComponentNamespace):
@staticmethod
def __call__(
- message: str, level: Optional[str] = None, **props
+ message: str = "", level: Optional[str] = None, **props
) -> "Optional[EventSpec]":
"""Send a toast message.
@@ -209,6 +213,9 @@ class ToastNamespace(ComponentNamespace):
level: The level of the toast.
**props: The options for the toast.
+ Raises:
+ ValueError: If the Toaster component is not created.
+
Returns:
The toast event.
"""
diff --git a/reflex/state.py b/reflex/state.py
index 49b5bd4a4..9313939dc 100644
--- a/reflex/state.py
+++ b/reflex/state.py
@@ -31,6 +31,8 @@ from typing import (
import dill
from sqlalchemy.orm import DeclarativeBase
+from reflex.config import get_config
+
try:
import pydantic.v1 as pydantic
except ModuleNotFoundError:
@@ -42,7 +44,6 @@ from redis.exceptions import ResponseError
from reflex import constants
from reflex.base import Base
-from reflex.config import get_config
from reflex.event import (
BACKGROUND_TASK_MARKER,
Event,
diff --git a/tests/test_state.py b/tests/test_state.py
index 2fc149389..c998944ef 100644
--- a/tests/test_state.py
+++ b/tests/test_state.py
@@ -19,6 +19,7 @@ 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 (
@@ -1527,7 +1528,6 @@ async def test_state_with_invalid_yield(capsys, mock_app):
Args:
capsys: Pytest fixture for capture standard streams.
mock_app: Mock app fixture.
-
"""
class StateWithInvalidYield(BaseState):
@@ -1546,10 +1546,29 @@ async def test_state_with_invalid_yield(capsys, mock_app):
rx.event.Event(token="fake_token", name="invalid_handler")
):
assert not update.delta
- assert update.events == rx.event.fix_events(
- [rx.window_alert("An error occurred. See logs for details.")],
- token="",
- )
+ if Toaster.is_used:
+ assert update.events == rx.event.fix_events(
+ [
+ rx.toast(
+ title="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"},
+ ) # type: ignore
+ ],
+ token="",
+ )
+ else:
+ assert update.events == rx.event.fix_events(
+ [
+ rx.window_alert(
+ "An error occurred.\nContact the website administrator."
+ )
+ ],
+ token="",
+ )
captured = capsys.readouterr()
assert "must only return/yield: None, Events or other EventHandlers" in captured.out