<p align="center"> <a href="https://dismissible.io" target="_blank"><img src="https://raw.githubusercontent.com/DismissibleIo/dismissible-api/main/docs/images/dismissible_logo.png" width="240" alt="Dismissible" /></a> </p>
npm install @dismissible/react-clientNever Show The Same Thing Twice!
Dismissible manages the state of your UI elements across sessions, so your users see what matters, once! No more onboarding messages reappearing on every tab, no more notifications haunting users across devices. Dismissible syncs dismissal state everywhere, so every message is intentional, never repetitive.
This is the React component library for creating dismissible UI elements with persistent state management.
This component is used with the Dismissible API Server, which you can self-host with Docker or integrate into your NestJS application.
dismissible.io | Documentation | API Server
- Easy to use - Simple component API for dismissible content
- Persistent state - Dismissal state is saved and restored across sessions when using the Dismissible API Server
- Automatic request batching - Multiple items requested in the same render cycle are automatically coalesced into a single API call
- Restore support - Restore previously dismissed items programmatically
- JWT Authentication - Built-in support for secure JWT-based authentication
- Custom HTTP Client - Bring your own HTTP client (axios, ky, etc.) with custom headers, interceptors, and tracking
- Customizable - Custom loading, error, and dismiss button components
- Accessible - Built with accessibility best practices
- Hook-based - Includes useDismissibleItem hook for custom implementations
- Lightweight - Minimal bundle size with tree-shaking support
- TypeScript - Full TypeScript support with complete type definitions
``bash`
npm install @dismissible/react-client
Make sure you have React 18+ installed:
`bash`
npm install react react-dom
First, you need a Dismissible API Server. The easiest way is with Docker:
`yaml`docker-compose.yml
version: '3.8'
services:
api:
image: dismissibleio/dismissible-api:latest
ports:
- '3001:3001'
environment:
DISMISSIBLE_PORT: 3001
`bash`
docker-compose up -d
OR
`bash`
docker run -p 3001:3001 -e DISMISSIBLE_PORT=3001 dismissibleio/dismissible-api:latest
See the API Server documentation for more deployment options including NestJS integration, public Docker image and more.
Wrap your app with DismissibleProvider. The userId prop is required to track all your dismissals per user:
`tsx
import { DismissibleProvider } from '@dismissible/react-client';
function App() {
const userId = getCurrentUserId();
return (
);
}
`
component, and the itemId, along with the userId, will become the unique key that is tracked across sessions and devices.`tsx
import { Dismissible } from '@dismissible/react-client';function WelcomeBanner() {
return (
Welcome to our app!
This banner can be dismissed and won't show again.
);
}
`API Reference
$3
Context provider that configures authentication and API settings. Required - all
components must be wrapped in a provider.#### Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
|
userId | string | ✅ | User ID for tracking dismissals per user |
| baseUrl | string | ✅ | API base URL for your self-hosted server |
| jwt | string \| (() => string) \| (() => Promise | ❌ | JWT token for secure authentication |
| client | DismissibleClient | ❌ | Custom HTTP client implementation (uses default if not provided) |
| children | ReactNode | ✅ | Components that will use the dismissible functionality |#### Example
`tsx
import { DismissibleProvider } from '@dismissible/react-client';// Basic setup with userId
function App() {
return (
);
}
// With static JWT
function AppWithJWT() {
return (
userId="user-123"
jwt="eyJhbGciOiJIUzI1NiIs..."
baseUrl="https://api.yourapp.com"
>
);
}
// With dynamic JWT function
function AppWithDynamicAuth() {
const { user, getAccessToken } = useAuth();
return (
userId={user.id}
jwt={() => getAccessToken()}
baseUrl="https://api.yourapp.com"
>
);
}
// With async JWT function
function AppWithAsyncAuth() {
const { user, refreshAndGetToken } = useAuth();
return (
userId={user.id}
jwt={async () => await refreshAndGetToken()}
baseUrl="https://api.yourapp.com"
>
);
}
`See Custom HTTP Client for examples of using a custom client.
$3
The main component for creating dismissible content.
> Note: The
component renders null when an item is dismissed. For restore functionality, use the useDismissibleItem hook directly in custom implementations.#### Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
|
itemId | string | ✅ | Unique identifier for the dismissible item |
| children | ReactNode | ✅ | Content to render when not dismissed |
| onDismiss | () => void | ❌ | Callback fired when item is dismissed |
| LoadingComponent | ComponentType<{itemId: string}> | ❌ | Custom loading component |
| ErrorComponent | ComponentType<{itemId: string, error: Error}> | ❌ | Custom error component |
| DismissButtonComponent | ComponentType<{onDismiss: () => Promise | ❌ | Custom dismiss button |
| ignoreErrors | boolean | ❌ | Ignore errors and display component anyway (default: false) |
| enableCache | boolean | ❌ | Enable localStorage caching (default: true) |
| cachePrefix | string | ❌ | Cache key prefix (default: 'dismissible') |
| cacheExpiration | number | ❌ | Cache expiration time in milliseconds |#### Example
`tsx
itemId="promo-banner"
onDismiss={() => console.log('Banner dismissed')}
>
Special Offer!
Get 50% off your first order
$3
For custom implementations and advanced use cases.
#### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
|
itemId | string | ✅ | Unique identifier for the dismissible item |
| options | object | ❌ | Configuration options |#### Options
| Option | Type | Required | Description |
|--------|------|----------|-------------|
|
enableCache | boolean | ❌ | Enable localStorage caching (default: true) |
| cachePrefix | string | ❌ | Cache key prefix (default: 'dismissible') |
| cacheExpiration | number | ❌ | Cache expiration time in milliseconds |
| initialData | IDismissibleItem | ❌ | Initial data for the dismissible item |#### Returns
| Property | Type | Description |
|----------|------|-------------|
|
dismissedAt | string \| undefined | ISO date string when item was dismissed, or undefined |
| dismiss | () => Promise | Function to dismiss the item |
| restore | () => Promise | Function to restore a dismissed item |
| isLoading | boolean | Loading state indicator |
| error | Error \| undefined | Error state, if any |
| item | IDismissibleItem \| undefined | The full dismissible item object |#### Example
`tsx
import { useDismissibleItem } from '@dismissible/react-client';function CustomDismissible({ itemId, children }) {
const { dismissedAt, dismiss, restore, isLoading, error } = useDismissibleItem(itemId);
if (isLoading) {
return
Loading...;
} if (error) {
return
Error: {error.message};
} if (dismissedAt) {
return (
This item was dismissed.
);
} return (
{children}
);
}
`Usage Examples
$3
`tsx
import { DismissibleProvider, Dismissible } from '@dismissible/react-client';function App() {
const userId = getCurrentUserId();
return (
);
}
function Dashboard() {
return (
Welcome!
Thanks for joining our platform. Here are some quick tips to get started.
);
}
`$3
For secure environments, configure JWT authentication:
`tsx
import { DismissibleProvider, Dismissible } from '@dismissible/react-client';function App() {
const { user, getAccessToken } = useAuth();
return (
userId={user.id}
jwt={() => getAccessToken()}
baseUrl="https://api.yourapp.com"
>
);
}
function Dashboard() {
return (
Welcome back!
You have 3 new notifications.
);
}
`$3
`tsx
import { Dismissible } from '@dismissible/react-client';const CustomDismissButton = ({ onDismiss, ariaLabel }) => (
onClick={onDismiss}
className="custom-close-btn"
aria-label={ariaLabel}
>
✕
);
function CustomBanner() {
return (
itemId="custom-banner"
DismissButtonComponent={CustomDismissButton}
>
This banner has a custom dismiss button!
$3
`tsx
import { Dismissible } from '@dismissible/react-client';const CustomLoader = ({ itemId }) => (
);const CustomError = ({ error }) => (
Oops! Something went wrong
{error.message}
);function AdvancedBanner() {
return (
itemId="advanced-banner"
LoadingComponent={CustomLoader}
ErrorComponent={CustomError}
>
This banner has custom loading and error states!
$3
`tsx
import { Dismissible } from '@dismissible/react-client';function Dashboard() {
return (
New feature: Dark mode is now available!
Scheduled maintenance: Sunday 2AM-4AM EST
Help us improve! Take our 2-minute survey.
);
}
`$3
`tsx
import { Dismissible } from '@dismissible/react-client';// Show content even if API fails
function RobustBanner() {
return (
itemId="important-announcement"
ignoreErrors={true}
>
Critical System Update
System maintenance scheduled for tonight. Please save your work.
);
}
`$3
`tsx
import { useDismissibleItem } from '@dismissible/react-client';
import { useState, useEffect } from 'react';function SmartNotification({ itemId, message, type = 'info' }) {
const { dismissedAt, dismiss, isLoading } = useDismissibleItem(itemId);
const [autoHide, setAutoHide] = useState(false);
// Auto-hide after 10 seconds for info messages
useEffect(() => {
if (type === 'info' && !dismissedAt) {
const timer = setTimeout(() => {
setAutoHide(true);
dismiss();
}, 10000);
return () => clearTimeout(timer);
}
}, [type, dismissedAt, dismiss]);
if (dismissedAt || autoHide) {
return null;
}
return (
notification notification-${type}}>
{message}
onClick={dismiss}
disabled={isLoading}
className="dismiss-btn"
>
{isLoading ? '...' : '×'}
);
}
`$3
Use the
restore function to bring back previously dismissed content:`tsx
import { useDismissibleItem } from '@dismissible/react-client';function RestorableBanner({ itemId }) {
const { dismissedAt, dismiss, restore, isLoading } = useDismissibleItem(itemId);
if (dismissedAt) {
return (
Banner was dismissed on {new Date(dismissedAt).toLocaleDateString()}
);
} return (
Welcome!
This is a restorable banner.
);
}
`Advanced Usage
$3
The library automatically batches multiple dismissible item requests into a single API call, dramatically reducing network overhead when rendering pages with many dismissible components.
#### How It Works
Under the hood, Dismissible uses a
BatchScheduler that implements DataLoader-style request coalescing:1. Request Collection: When multiple
components or useDismissibleItem hooks mount during the same render cycle, each request is queued rather than fired immediately.2. Microtask Scheduling: The scheduler uses
queueMicrotask() to defer execution until after all synchronous code in the current JavaScript tick completes.3. Batch Execution: All queued requests are combined into a single batch API call (up to 50 items per batch, with automatic splitting for larger sets).
4. Result Distribution: When the API responds, results are distributed back to each waiting component.
`
┌─────────────────────────────────────────────────────────────────┐
│ Same JavaScript Tick │
├─────────────────────────────────────────────────────────────────┤
│ Component A Component B Component C │
│ requests "banner" requests "modal" requests "tooltip" │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ BatchScheduler│ │
│ │ Queue │ │
│ └───────┬───────┘ │
│ │ │
│ queueMicrotask │
└────────────────────────────┼────────────────────────────────────┘
▼
┌─────────────────────────┐
│ Single API Call │
│ POST /v1/users/{id}/ │
│ items/batch │
│ ["banner", "modal", │
│ "tooltip"] │
└─────────────────────────┘
`#### Example: Dashboard with Multiple Dismissibles
`tsx
// Without batching: 5 separate API calls
// With batching: 1 single API call containing all 5 item IDsfunction Dashboard() {
return (
);
}
`#### Built-in Optimizations
The
BatchScheduler includes several optimizations:- Request Deduplication: If the same
itemId is requested multiple times in the same tick, only one request is made and the result is shared.
- In-Memory Caching: Previously fetched items are cached in memory to avoid redundant API calls.
- Cache Priming: Items loaded from localStorage are automatically primed in the batch cache.
- Cache Sync: When items are dismissed or restored, the batch cache is updated to ensure consistency.#### Using the Hook with Batching
The batching is completely transparent when using the
useDismissibleItem hook:`tsx
function NotificationCenter() {
// All three hooks will batch their requests into a single API call
const notification1 = useDismissibleItem('notification-1');
const notification2 = useDismissibleItem('notification-2');
const notification3 = useDismissibleItem('notification-3'); // Rendering logic...
}
`#### Performance Impact
| Scenario | Without Batching | With Batching |
|----------|------------------|---------------|
| 5 dismissible items | 5 HTTP requests | 1 HTTP request |
| 20 dismissible items | 20 HTTP requests | 1 HTTP request |
| 100 dismissible items | 100 HTTP requests | 2 HTTP requests* |
\ Batches are automatically split at 50 items to respect API limits*
$3
By default, Dismissible uses a built-in HTTP client powered by
openapi-fetch. However, you can provide your own HTTP client implementation by passing a client prop to the DismissibleProvider. This is useful when you need:- Custom headers (correlation IDs, tracing, etc.)
- Request/response interceptors
- Use a different HTTP library (axios, ky, etc.)
- Analytics and logging
- Custom error handling
#### The DismissibleClient Interface
Your custom client must implement the
DismissibleClient interface:`typescript
import type { DismissibleClient, DismissibleItem } from '@dismissible/react-client';interface DismissibleClient {
getOrCreate: (params: {
userId: string;
itemId: string;
baseUrl: string;
authHeaders: { Authorization?: string };
signal?: AbortSignal;
}) => Promise;
// Required for automatic batching - fetches multiple items in one API call
batchGetOrCreate: (params: {
userId: string;
itemIds: string[]; // Array of item IDs (max 50)
baseUrl: string;
authHeaders: { Authorization?: string };
signal?: AbortSignal;
}) => Promise;
dismiss: (params: {
userId: string;
itemId: string;
baseUrl: string;
authHeaders: { Authorization?: string };
}) => Promise;
restore: (params: {
userId: string;
itemId: string;
baseUrl: string;
authHeaders: { Authorization?: string };
}) => Promise;
}
`> Note: The
batchGetOrCreate method is essential for the automatic request batching feature. When multiple components request items in the same render cycle, this method is called instead of multiple getOrCreate calls.#### Example: Custom Client with Axios
`tsx
import axios from 'axios';
import { v4 as uuid } from 'uuid';
import { DismissibleProvider } from '@dismissible/react-client';
import type { DismissibleClient } from '@dismissible/react-client';const axiosClient: DismissibleClient = {
getOrCreate: async ({ userId, itemId, baseUrl, authHeaders, signal }) => {
const response = await axios.get(
${baseUrl}/v1/users/${userId}/items/${itemId},
{
headers: {
...authHeaders,
'X-Correlation-ID': uuid(),
},
signal,
}
);
return response.data.data;
}, // Batch endpoint for automatic request coalescing
batchGetOrCreate: async ({ userId, itemIds, baseUrl, authHeaders, signal }) => {
const response = await axios.post(
${baseUrl}/v1/users/${userId}/items/batch,
{ itemIds },
{
headers: {
...authHeaders,
'X-Correlation-ID': uuid(),
},
signal,
}
);
return response.data.data;
}, dismiss: async ({ userId, itemId, baseUrl, authHeaders }) => {
const response = await axios.delete(
${baseUrl}/v1/users/${userId}/items/${itemId},
{
headers: {
...authHeaders,
'X-Correlation-ID': uuid(),
},
}
);
return response.data.data;
}, restore: async ({ userId, itemId, baseUrl, authHeaders }) => {
const response = await axios.post(
${baseUrl}/v1/users/${userId}/items/${itemId},
{},
{
headers: {
...authHeaders,
'X-Correlation-ID': uuid(),
},
}
);
return response.data.data;
},
};function App() {
return (
userId="user-123"
baseUrl="https://api.yourapp.com"
client={axiosClient}
>
);
}
`#### Example: Custom Client with Logging
`tsx
import type { DismissibleClient } from '@dismissible/react-client';const loggingClient: DismissibleClient = {
getOrCreate: async ({ userId, itemId, baseUrl, authHeaders, signal }) => {
console.log(
[Dismissible] Fetching item: ${itemId} for user: ${userId});
const startTime = performance.now(); const response = await fetch(
${baseUrl}/v1/users/${userId}/items/${itemId},
{
method: 'GET',
headers: authHeaders,
signal,
}
); const data = await response.json();
console.log(
[Dismissible] Fetched in ${performance.now() - startTime}ms); if (!response.ok) {
throw new Error(data.message || 'Failed to fetch dismissible item');
}
return data.data;
},
batchGetOrCreate: async ({ userId, itemIds, baseUrl, authHeaders, signal }) => {
console.log(
[Dismissible] Batch fetching ${itemIds.length} items for user: ${userId});
const startTime = performance.now(); const response = await fetch(
${baseUrl}/v1/users/${userId}/items/batch,
{
method: 'POST',
headers: {
...authHeaders,
'Content-Type': 'application/json',
},
body: JSON.stringify({ itemIds }),
signal,
}
); const data = await response.json();
console.log(
[Dismissible] Batch fetched ${itemIds.length} items in ${performance.now() - startTime}ms); if (!response.ok) {
throw new Error(data.message || 'Failed to batch fetch dismissible items');
}
return data.data;
},
dismiss: async ({ userId, itemId, baseUrl, authHeaders }) => {
console.log(
[Dismissible] Dismissing item: ${itemId});
const response = await fetch(
${baseUrl}/v1/users/${userId}/items/${itemId},
{
method: 'DELETE',
headers: authHeaders,
}
); const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to dismiss item');
}
console.log(
[Dismissible] Item dismissed at: ${data.data.dismissedAt});
return data.data;
}, restore: async ({ userId, itemId, baseUrl, authHeaders }) => {
console.log(
[Dismissible] Restoring item: ${itemId});
const response = await fetch(
${baseUrl}/v1/users/${userId}/items/${itemId},
{
method: 'POST',
headers: authHeaders,
}
); const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to restore item');
}
console.log(
[Dismissible] Item restored successfully);
return data.data;
},
};function App() {
return (
userId="user-123"
baseUrl="https://api.yourapp.com"
client={loggingClient}
>
);
}
`#### Example: Custom Client with Retry Logic
`tsx
import type { DismissibleClient } from '@dismissible/react-client';async function fetchWithRetry(
url: string,
options: RequestInit,
retries = 3
): Promise {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok || attempt === retries) {
return response;
}
} catch (error) {
if (attempt === retries) throw error;
}
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 100));
}
throw new Error('Max retries reached');
}
const retryClient: DismissibleClient = {
getOrCreate: async ({ userId, itemId, baseUrl, authHeaders, signal }) => {
const response = await fetchWithRetry(
${baseUrl}/v1/users/${userId}/items/${itemId},
{ method: 'GET', headers: authHeaders, signal }
);
const data = await response.json();
return data.data;
}, batchGetOrCreate: async ({ userId, itemIds, baseUrl, authHeaders, signal }) => {
const response = await fetchWithRetry(
${baseUrl}/v1/users/${userId}/items/batch,
{
method: 'POST',
headers: { ...authHeaders, 'Content-Type': 'application/json' },
body: JSON.stringify({ itemIds }),
signal,
}
);
const data = await response.json();
return data.data;
}, dismiss: async ({ userId, itemId, baseUrl, authHeaders }) => {
const response = await fetchWithRetry(
${baseUrl}/v1/users/${userId}/items/${itemId},
{ method: 'DELETE', headers: authHeaders }
);
const data = await response.json();
return data.data;
}, restore: async ({ userId, itemId, baseUrl, authHeaders }) => {
const response = await fetchWithRetry(
${baseUrl}/v1/users/${userId}/items/${itemId},
{ method: 'POST', headers: authHeaders }
);
const data = await response.json();
return data.data;
},
};
`Styling
The library includes minimal default styles. You can override them or provide your own:
`css
/ Default classes you can style /
.dismissible-container {
/ Container for the dismissible content /
}.dismissible-loading {
/ Loading state /
}
.dismissible-error {
/ Error state /
}
.dismissible-button {
/ Default dismiss button /
}
`Self-Hosting
Dismissible is designed to be self-hosted. You have full control over your data.
$3
The fastest way to get started:
`bash
docker run -p 3001:3001 dismissibleio/dismissible-api:latest
`See the Docker documentation for production configuration.
$3
Integrate directly into your existing NestJS application:
`bash
npm install @dismissible/nestjs-api
``See the NestJS documentation for setup instructions.
- Documentation
- GitHub - React Client
- GitHub - API Server
MIT © Dismissible
See CHANGELOG.md for a detailed list of changes.