Skip to main content
Deno 2 is finally here πŸŽ‰οΈ
Learn more

What is Fluxy?

Fluxy is a very simple store that allows Deno Fresh islands to communicate using hooks.

Usage

Custom Hook

// hooks/useToaster.ts
import { useStore } from "https://deno.land/x/fluxy/mod.ts";

export interface ToastState {
  show: boolean;
  type: "success" | "warning" | "error";
  message: string;
}

const defaultToastState = (): ToastState => ({
  show: false,
  type: "success",
  message: "",
});

export const useToaster = () => {
  const { state, setState, resetState } = useStore(
    "toast-notification",
    defaultToastState,
  );

  const showToast = (
    { type, message }: Pick<ToastState, "message" | "type">,
  ) => {
    if (state.value.show) {
      resetState();
    }
    setState({
      show: true,
      type,
      message,
    });
  };

  const hideToast = () => {
    resetState();
  };

  return {
    state,
    showToast,
    hideToast,
  };
};

Islands

Toast Component

// islands/Toast.tsx
import { useMemo } from "preact/hooks";
import { useToaster } from "../hooks/useToaster.ts";

export default function Toast() {
  const { state, hideToast } = useToaster();
  const { show, type, message } = state.value;

  const icon = useMemo(() => {
    switch (type) {
      case "success": {
        return (
          <svg
            class="flex-shrink-0 h-4 w-4 text-green-500 mt-0.5"
            xmlns="http://www.w3.org/2000/svg"
            width="16"
            height="16"
            fill="currentColor"
            viewBox="0 0 16 16"
          >
            <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
          </svg>
        );
      }
      case "warning": {
        return (
          <svg
            class="flex-shrink-0 h-4 w-4 text-yellow-500 mt-0.5"
            xmlns="http://www.w3.org/2000/svg"
            width="16"
            height="16"
            fill="currentColor"
            viewBox="0 0 16 16"
          >
            <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2z" />
          </svg>
        );
      }
      case "error": {
        return (
          <svg
            class="flex-shrink-0 h-4 w-4 text-red-500 mt-0.5"
            xmlns="http://www.w3.org/2000/svg"
            width="16"
            height="16"
            fill="currentColor"
            viewBox="0 0 16 16"
          >
            <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
          </svg>
        );
      }
      default: {
        return <></>;
      }
    }
  }, [type]);

  return show
    ? (
      <div
        class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 max-w-xs bg-white border border-gray-200 rounded-xl shadow-lg"
        role="alert"
      >
        <div class="flex p-4 items-center gap-3">
          <div class="flex-shrink-0">
            {icon}
          </div>
          <div class="ms-3">
            <p class="text-sm text-gray-700 dark:text-gray-400">
              {message}
            </p>
          </div>
          <button
            type="button"
            class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8"
            onClick={hideToast}
          >
            <span class="sr-only">Close</span>
            <svg
              class="w-3 h-3"
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 14 14"
            >
              <path
                stroke="currentColor"
                stroke-linecap="round"
                stroke-linejoin="round"
                stroke-width="2"
                d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
              />
            </svg>
          </button>
        </div>
      </div>
    )
    : <></>;
}

Toaster Control

// islands/ShowToastButton.tsx
import { faker } from "https://deno.land/x/deno_faker@v1.0.3/mod.ts";
import { useToaster } from "../hooks/useToaster.ts";

export default function ShowToastButton(props: { type: "show" | "hide" }) {
  const { showToast, hideToast } = useToaster();

  return props.type === "show"
    ? (
      <button
        class="text-white bg-yellow-700 hover:bg-yellow-800 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2"
        onClick={() => {
          function getRandomInt(min: number, max: number) {
            return Math.floor(Math.random() * (max - min + 1) + min);
          }

          const index = getRandomInt(0, 2);
          const values: ["success", "warning", "error"] = [
            "success",
            "warning",
            "error",
          ];
          showToast({
            type: values.at(index)!,
            message: faker.hacker.phrase(),
          });
        }}
      >
        Show Toast
      </button>
    )
    : (
      <button
        class="text-white bg-yellow-700 hover:bg-yellow-800 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2"
        onClick={hideToast}
      >
        Hide Toast
      </button>
    );
}

Examples

Take a look at the following files in this repo for a reference implementation.

β”œβ”€β”€ hooks
β”‚   └── useToaster.ts
β”œβ”€β”€ islands
β”‚   β”œβ”€β”€ ShowToastButton.tsx
β”‚   └── Toast.tsx
└── routes
    └── _app.tsx

Code Sandbox

Demo

Live Demo

Design Doc