@usefy/use-permission
v0.25.1
Published
A React hook for reading Permissions API status with live updates, SSR-safe and typed
Maintainers
Readme
Overview
usePermission is part of the @usefy ecosystem — a collection of production-ready, TypeScript-first, SSR-safe React hooks. It reads the Permissions API status of a permission and keeps it up to date live, so your UI reacts when the user grants or revokes access.
Features
- Live updates — subscribes to the
PermissionStatuschangeevent, sostatereflects grant/revoke without polling or a re-mount. - Rich, honest return —
{ state, status, isSupported, error }makes the async, unsupported, and error paths explicit instead of hiding them behind a barePermissionState. - Any permission name — accepts a superset of
PermissionDescriptor, socamera,microphone,push,midi, and future names all typecheck. - Stable descriptor identity — keyed on the descriptor's serialized contents, so an inline
{ name: 'camera' }literal does not re-query every render (no caller-sideuseMemo). - SSR-safe & StrictMode-safe — reports
unsupportedon the server; the async query is race-guarded and the change listener is cleaned up on unmount. - TypeScript-first — full type inference and exported types.
- Tiny & tree-shakeable — zero dependencies, published as its own package.
Installation
# npm
npm install @usefy/use-permission
# yarn
yarn add @usefy/use-permission
# pnpm
pnpm add @usefy/use-permissionRequires React 18 or 19 (peerDependencies: "react": "^18.0.0 || ^19.0.0").
Quick Start
import { usePermission } from "@usefy/use-permission";
function CameraStatus() {
const { state, status } = usePermission({ name: "camera" });
// Branch on `status` (deterministic "idle" on the first render, both on the
// server and client) so the UI is hydration-safe.
if (status === "idle" || status === "pending") return <span>Checking…</span>;
if (status === "unsupported") return <span>Permissions API unavailable</span>;
if (status === "error") return <span>Could not read camera permission</span>;
return <span>Camera: {state}</span>; // 'granted' | 'denied' | 'prompt'
}API
usePermission(descriptor)
function usePermission(descriptor: UsePermissionDescriptor): UsePermissionReturn;Parameters
| Param | Type | Description |
| ----- | ---- | ----------- |
| descriptor | UsePermissionDescriptor | The permission to query, e.g. { name: 'camera' }, { name: 'geolocation' }, { name: 'push', userVisibleOnly: true }, { name: 'midi', sysex: true }. name accepts the standard PermissionName values (with autocomplete) plus any browser-specific string. Pass an inline literal freely — the hook keys re-queries on the descriptor's serialized contents, not its object identity. |
Returns — UsePermissionReturn
| Field | Type | Description |
| ----- | ---- | ----------- |
| state | PermissionState \| null | The raw permission state ('granted' \| 'denied' \| 'prompt'), or null until the first query resolves and whenever the API is unsupported or the query errored. |
| status | UsePermissionStatus | Coarse lifecycle: 'idle' \| 'pending' \| 'granted' \| 'denied' \| 'prompt' \| 'unsupported' \| 'error'. The three permission values mirror state, so you can branch on either. |
| isSupported | boolean | Whether navigator.permissions.query is available. Starts false (on the server and the first client render, for hydration safety) and becomes true after mount in a supporting browser. Branch your first render on status ('idle'), not isSupported. |
| error | Error \| null | The error thrown by navigator.permissions.query(), if the query rejected (e.g. an unknown permission name in a browser that throws). |
Helpers
isPermissionsSupported(): boolean— capability check used internally; exported for feature-detection.serializeDescriptor(descriptor): string— the stable-key function the hook uses to decide when to re-query (exported for advanced use; not part of the umbrella surface).
Notes on behavior
- Descriptor identity.
usePermission({ name: 'camera' })receives a fresh object literal every render. Rather than require callers touseMemoit, the hook derives a stable string key from the descriptor's own fields (sorted, JSON-serialized) and keys its effect on that. It re-queries only when the meaningful contents change, e.g.name'camera'→'microphone'oruserVisibleOnlytrue→false. - Unsupported. If
navigator.permissionsis missing (SSR or an unsupporting browser),statusis'unsupported',isSupportedisfalse, andstateisnull. - Errors. Some browsers reject
query()for permission names they don't recognize (e.g.camerain Firefox) instead of returning'denied'. That surfaces asstatus: 'error'with the thrownerror. - Race safety. If the component unmounts while a query is in flight, the stale resolution/rejection is ignored — no state update after unmount — and the
changelistener is always removed on cleanup.
Testing
📊 View Detailed Coverage Report (GitHub Pages) — 18 tests, 100% statement coverage.
License
MIT © mirunamu
This package is part of the usefy monorepo.
