Skip to content

Commit

Permalink
version: 0.1.0
Browse files Browse the repository at this point in the history
Added `useToasts` hook, refactored into using it internally in `ToastProvider`.
  • Loading branch information
imp-dance committed Apr 23, 2023
1 parent 23979ca commit 530939b
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 58 deletions.
45 changes: 40 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ A simple headless toast solution for your React project.

## Quick guide

You can render the toasts using either `useToasts` or `ToastProvider`, returned from `initToast`. `ToastProvider` uses `useToasts` internally, and is just a convenient wrapper.

```tsx
// toast.ts
import { initToast } from "@ryfylke-react/toast";
Expand All @@ -27,12 +29,15 @@ const App = () => {
return (
<>
<ToastProvider
portal={document.body}
removeToastsAfterMs={3000}
renderToasts={(props) => {
return (
<div>
<div className="toasts-container">
{props.toasts.map((toast) => (
<button
key={toast.id}
className="toast"
onClick={() => props.onRemoveToast(toast.id)}
>
{toast.title}
Expand Down Expand Up @@ -65,15 +70,45 @@ export const Elsewhere = () => {
};
```

## Arguments
## Reference

### `initToast<T>`

**Arguments**:
Takes one [Typescript generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) to specify the desired toast interface.

**Returns**:

The following (`toast`, `useToasts` & `ToastProvider`):

### `toast`

Takes whatever interface you specified as a generic when initializing with `initToast`.

### `useToasts`

**Arguments**:

- `onToastAdded` - _(optional)_ A function that is run whenever a new toast is dispatched. Returns the toast in its argument. Useful for logging the toasts, or syncing them with an external store.
- `removeToastsAfterMs` - _(optional)_ Determines if toasts should be removed from the list, and how long they should stay. (`number | undefined`)

**Returns**:

- `toasts` - A list of all current toasts.
- `onRemoveToast` - A function that takes one argument, `toastId: string`, and removes the given toast from the internal list.

### `ToastProvider`

**Arguments**:
Same arguments as `useToasts`, but laid out in a different manner:

- `renderToasts` - Takes a function that returns a ReactElement. Has the following props:
- `toasts` - The list of toasts
- `onRemoveToast` - A function that takes one argument, `toastId: string`.
- `removeToastsAfterMs` - An optional argument that lets you configure a timeout for automatically removing the toast from the list.
- `removeToastsAfterMs` - _(optional)_ An argument that lets you configure a timeout for automatically removing the toast from the list.
- `onToastAdded` - _(optional)_ A function that is run whenever a new toast is dispatched. Returns the toast in its argument. Useful for logging the toasts, or syncing them with an external store.
- `portal` - _(optional)_ If supplied, specifies which HTMLElement to render a portal to for `renderToasts`. If not supplied, no portal is rendered.

### `toast`
**Returns**: Whatever `renderToasts` returns.

Takes whatever interface you specified as a generic when initializing.
**Notes**: `ToastProvider` is not strictly nessecary if you are using `useToasts`. This component is just a helper/HOC for utilizing the hook.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ryfylke-react/toast",
"version": "0.0.2",
"version": "0.1.0",
"description": "A simple solution for building your own toast management system.",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
Expand Down
72 changes: 20 additions & 52 deletions src/components/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,39 @@
import React, {
ReactElement,
useEffect,
useRef,
useState,
} from "react";
import { ToastEventType } from "../toast";
import React, { ReactElement } from "react";
import ReactDOM from "react-dom";
import { useToasts } from "../useToasts";

export type ToastProviderProps<T> = {
renderToasts: (props: {
toasts: (T & { id: string })[];
onRemoveToast: (id: string) => void;
}) => ReactElement;
removeToastsAfterMs?: number;
};

let incr = 0;
const genId = () => {
incr += 1;
return `toast-${incr}`;
onToastAdded?: (toast: T) => void;
portal?: undefined | HTMLElement;
};

export function ToastProvider<T extends Record<string, any>>(
props: ToastProviderProps<T>
) {
const mounted = useRef(true);
const [toasts, setToasts] = useState<(T & { id: string })[]>(
[]
);
useEffect(() => {
mounted.current = true;
const listener = (ev: Event) => {
const event = ev as Event & { detail?: T };
if (event.detail) {
const toastId = genId();
setToasts((prev) => {
if (event.detail) {
return [...prev, { ...event.detail, id: toastId }];
} else {
return prev;
}
});
if (props.removeToastsAfterMs) {
setTimeout(() => {
if (mounted.current) {
setToasts((prev) =>
prev.filter((item) => item.id !== toastId)
);
}
}, props.removeToastsAfterMs);
}
}
};
document.addEventListener(ToastEventType, listener);
return () => {
document.removeEventListener(ToastEventType, listener);
mounted.current = false;
};
}, [props.removeToastsAfterMs]);
const { toasts, onRemoveToast } = useToasts({
removeToastsAfterMs: props.removeToastsAfterMs,
onToastAdded: props.onToastAdded,
});

if (props.portal) {
return ReactDOM.createPortal(
<props.renderToasts
toasts={toasts}
onRemoveToast={onRemoveToast}
/>,
props.portal
);
}

return (
<props.renderToasts
toasts={toasts}
onRemoveToast={(id: string) => {
setToasts((prev) =>
prev.filter((item) => item.id !== id)
);
}}
onRemoveToast={onRemoveToast}
/>
);
}
2 changes: 2 additions & 0 deletions src/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ToastProvider,
ToastProviderProps,
} from "./components/ToastProvider";
import { UseToastsOpts, useToasts } from "./useToasts";

export const ToastEventType = "ryfrea-toast" as const;

Expand All @@ -16,5 +17,6 @@ export const initToast = <T extends Record<string, any>>() => {
ToastProvider: (props: ToastProviderProps<T>) => (
<ToastProvider {...props} />
),
useToasts: (opts: UseToastsOpts<T>) => useToasts<T>(opts),
};
};
56 changes: 56 additions & 0 deletions src/useToasts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEffect, useRef, useState } from "react";
import { ToastEventType } from "./toast";
import { genId } from "./utils";

export type UseToastsOpts<T> = {
onToastAdded?: (toast: T & { id: string }) => void;
removeToastsAfterMs?: number;
};

/** Listens to all toasts and stores them in a list */
export function useToasts<T>(opts: UseToastsOpts<T> = {}) {
const mounted = useRef(true);
const [toasts, setToasts] = useState<(T & { id: string })[]>(
[]
);
useEffect(() => {
mounted.current = true;
const listener = (ev: Event) => {
const event = ev as Event & { detail?: T };
if (event.detail) {
const toastId = genId();
setToasts((prev) => {
if (event.detail) {
const newToast = { ...event.detail, id: toastId };
opts.onToastAdded?.(newToast);
return [...prev, { ...event.detail, id: toastId }];
} else {
return prev;
}
});
if (opts.removeToastsAfterMs) {
setTimeout(() => {
if (mounted.current) {
setToasts((prev) =>
prev.filter((item) => item.id !== toastId)
);
}
}, opts.removeToastsAfterMs);
}
}
};
document.addEventListener(ToastEventType, listener);
return () => {
document.removeEventListener(ToastEventType, listener);
mounted.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [opts.removeToastsAfterMs, opts.onToastAdded]);

return {
toasts,
onRemoveToast: (id: string) => {
setToasts((prev) => prev.filter((item) => item.id !== id));
},
};
}
5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
let incr = 0;
export const genId = () => {
incr += 1;
return `toast-${incr}`;
};

0 comments on commit 530939b

Please sign in to comment.