npm package: @sentinel-js/react
Description: A lightweight React SDK that polls for application updates and handles them gracefully. It supports a ready-made toast banner, **silent updates** (reload on next navigation with no UI), and a headless API for custom UIs.
Latest version: 0.1.6

# @sentinel-js/react

A lightweight React SDK that polls for application updates and handles them gracefully. It supports a ready-made toast banner, **silent updates** (reload on next navigation with no UI), and a headless API for custom UIs.

> **Important:** This package is designed to work with **`@sentinel-js/vite-plugin`**. The plugin injects the current build version and writes a `version.json` file; without it, the SDK will report version as `"unknown"` and log a console warning.

## Installation

```bash
npm install @sentinel-js/react
# or
pnpm add @sentinel-js/react
```

**Peer dependencies:** `react` and `react-dom` (>=16.8.0). You must have them installed in your app.

When you add `@sentinel-js/vite-plugin` to your Vite config, it automatically sets `resolve.dedupe` and `optimizeDeps.include` so a single React instance is used in both dev and build. No extra config is required.

## Usage

### Option A: Standard UI (Toast Banner)

The simplest setup: add `<SentinelToast />` and it will poll for updates and show a "New version available" banner with a "Refresh" button.

```tsx
import { SentinelToast } from '@sentinel-js/react';

function App() {
  return (
    <div>
      <AppRoutes />
      <SentinelToast style={{ right: '20px', bottom: '20px' }} />
    </div>
  );
}
```

The toast is fixed at bottom-right by default; use `style` or `className` to customize.

### Option B: Silent Update (Zero UI)

No banner: the SDK waits for a **client-side route change** (e.g. user clicks a link), then performs a full page reload so the next view is the new version. Good for apps where you want updates without interrupting the user.

```tsx
import { useSentinel } from '@sentinel-js/react';

function App() {
  useSentinel({
    silent: true,
    pollingInterval: 15000, // Check every 15 seconds
  });

  return <AppRoutes />;
}
```

### Option C: Custom UI (Headless)

Use the hook and build your own notification (e.g. with `react-hot-toast`, `sonner`, or a custom modal).

```tsx
import { useSentinel } from '@sentinel-js/react';

function App() {
  const { hasUpdate, reload, currentVersion, remoteVersion } = useSentinel({
    onUpdateAvailable: (version) => console.log('New version:', version),
  });

  if (hasUpdate) {
    return (
      <div className="update-banner">
        New version available! (current: {currentVersion}, new: {remoteVersion})
        <button onClick={reload}>Refresh now</button>
      </div>
    );
  }

  return <AppRoutes />;
}
```

## API Reference

### `useSentinel(config?)`

Hook that polls the version file, compares with the current build version, and exposes update state and a reload function.

**Returns:**

| Property | Type | Description |
|----------|------|-------------|
| `hasUpdate` | `boolean` | `true` when a newer version has been detected. |
| `currentVersion` | `string` | Version of the current build (injected by the Vite plugin, or `'unknown'` without it). |
| `remoteVersion` | `string \| null` | Version string from the server, or `null` before first successful check. |
| `reload` | `() => void` | Calls `window.location.reload()`. Use after showing your own "Refresh" UI. |
| `checkNow` | `() => Promise<void>` | Manually trigger a version check. |

### Configuration (shared by `useSentinel` and `<SentinelToast />`)

| Prop | Type | Default | Description |
|------|------|--------|-------------|
| `pollingInterval` | `number` | `60000` | How often (in ms) to poll the version file. |
| `versionFileUrl` | `string` | `'/version.json'` | URL path to the version file (same as generated by the Vite plugin). |
| `silent` | `boolean` | `false` | If `true`, no UI is shown; reload happens on next route change (when an update was detected). |
| `checkOnFocus` | `boolean` | `true` | Run a version check when the window gains focus. |
| `onUpdateAvailable` | `(version: string) => void` | `undefined` | Callback when a new version is detected (e.g. for analytics or custom UI). |

### `<SentinelToast />` (UI-only props)

In addition to the config props above, the toast component accepts:

| Prop | Type | Description |
|------|------|-------------|
| `style` | `React.CSSProperties` | Override default positioning and styling. |
| `className` | `string` | Additional CSS class names. |

If `silent` is `true`, `<SentinelToast />` renders nothing.

## How it works

1. **Build time:** `@sentinel-js/vite-plugin` injects a global `__SENTINEL_VERSION__` (a short hash) and writes `version.json` (e.g. in `dist/`) with the same version.
2. **Runtime:** The SDK reads `__SENTINEL_VERSION__` as the current version and periodically fetches `versionFileUrl` (with a cache-busting query param).
3. **Update detection:** When the remote version differs from the current one, the SDK sets `hasUpdate: true` and either shows the toast (default) or waits for a route change to reload (silent mode).

## Troubleshooting

- **"Current version is unknown. Ensure @sentinel/vite-plugin is installed."**  
  The app was built without `@sentinel-js/vite-plugin`, or the plugin’s `define` did not run (e.g. wrong hook order). Ensure the plugin is in your Vite config and you run a production build (`vite build`). See [@sentinel-js/vite-plugin](../vite-plugin/README.md).

- **Multiple React instances / "Cannot read properties of null (reading 'useState')"**  
  If you use the built package in an app that also bundles React, ensure a single React instance (e.g. Vite `resolve.dedupe: ['react', 'react-dom']`). In monorepos, aliasing the library to source in the app’s Vite config can avoid duplicate React.

## License

ISC