usePermission
Browser & devicePermissions API status with live updates — SSR-safe, typed, and race-safe.
Install
$ npm install @usefy/use-permissionQuick start
usePermission.tsx
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 reference
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.
Go deeper
This page is the quick reference. For every example, prop, and edge case, read the full README — or open Storybook to change props live.