githubEdit

message-checkNotifications

Toast notifications

Shopfoo displays toast notifications to give feedback after save operations (success or error). Unlike many applications that rely on a dedicated toast library (e.g. react-toastify), Shopfoo uses a custom Toast React component built with DaisyUI's toast and alert primitives.

How it works

The toast mechanism is implemented entirely through the Elmish model at the application level (View.fs):

1. Toast type (Shared.fs): A discriminated union models each kind of toast, carrying the relevant data and an optional ApiError.

[<RequireQualifiedAccess>]
type Toast =
    | Lang of Lang
    | Prices of Prices * ApiError option
    | Product of Product * ApiError option
    | Sale of SKU * ApiError option
    | Stock of Stock * ApiError option
    | Supply of SKU * ApiError option

2. App model (View.fs): The root AppView component holds a Toast option in its Elmish model. The update function handles ToastOn and ToastOff messages.

type private Msg =
    | ...
    | ToastOn of Toast
    | ToastOff

type private Model = {
    ...
    Toast: Toast option
}

let private update (msg: Msg) (model: Model) =
    match msg with
    | ...
    | Msg.ToastOn toast -> { model with Toast = Some toast }, Cmd.none
    | Msg.ToastOff -> { model with Toast = None }, Cmd.none

3. Env abstraction (Shared.fs): An IShowToast interface lets child pages and components trigger a toast without depending on the root component, following the Dependency Inversion Principle.

4. Child components: After a save operation completes, the child calls env.ShowToast(Toast.Xxx(...)) from a Cmd.ofEffect. The effect passes the result (with or without error) up to the root, which renders the appropriate toast.

5. Toast rendering (View.fs): The root view pattern-matches on model.Toast to render the appropriate toast. A local helper builds the alert with the right style (success/error) and dismiss mode.

6. Toast component (Components/Toast.fs): A React component that renders a DaisyUI alert positioned as a toast. It supports two dismiss modes:

  • Auto: the toast disappears after a timeout (3 seconds).

  • Manual: the toast stays visible with a close button — used for errors.

Cmd.ofEffect trade-off

Child components use Cmd.ofEffect to call env.ShowToast (or other environment callbacks like env.FillTranslations) from their update function. This is a pragmatic choice: it avoids threading toast messages through each component's own Msg type and keeps the communication simple.

Jordan Marrarrow-up-right also suggests using the Cmd track for toast notifications. His approach wraps Toastify calls behind helper commands (Cmd.error, Cmd.info…), hiding the library dependency. However, because toast display is handled entirely through side-effect commands, there is no model state to assert on in tests.

Shopfoo takes a different approach: the Toast is stored in the Elmish model (Toast option), which means the toast state is observable and testable — e.g. verifying that after a save error, the model contains the expected Toast.Prices(_, Some error). This makes the rendering logic deterministic and unit-testable without involving the DOM.

circle-exclamation

Last updated