Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | 42x 42x 42x 42x 42x 42x 42x 42x 13x 13x 13x 2x 2x 2x 2x 11x 11x 11x 11x 11x 11x 11x 8x 7x 7x 7x 7x 2x 2x 7x 3x 2x 2x 2x 11x 11x 11x 7x 42x 38x | import { useEffect, useMemo, useRef, useState } from "react";
import type {
UsePermissionDescriptor,
UsePermissionReturn,
UsePermissionStatus,
} from "./types";
import { isPermissionsSupported, serializeDescriptor } from "./utils";
/**
* Track the status of a Permissions API permission, with live updates.
*
* Calls `navigator.permissions.query(descriptor)` and subscribes to the returned
* `PermissionStatus`'s `change` event, so the returned `state` reflects the user
* granting or revoking the permission without a re-mount. The hook is SSR-safe
* (reports `unsupported` on the server) and StrictMode/concurrent-safe (the async
* query is race-guarded and the listener is cleaned up on unmount).
*
* **Descriptor identity:** the effect is keyed on a *serialized* copy of the
* descriptor's fields (`name`, `userVisibleOnly`, `sysex`, …), not on the object
* reference. So passing an inline literal — `usePermission({ name: 'camera' })` —
* does not re-query on every render; it re-queries only when the descriptor's
* contents actually change. No `useMemo` on the caller's side is required.
*
* @param descriptor - The permission to query, e.g. `{ name: 'camera' }`,
* `{ name: 'geolocation' }`, `{ name: 'push', userVisibleOnly: true }`,
* `{ name: 'midi', sysex: true }`. Accepts any permission name (the standard
* ones plus browser-specific ones like `'camera'`/`'microphone'`).
* @returns `{ state, status, isSupported, error }` — see {@link UsePermissionReturn}.
*
* @example
* ```tsx
* function CameraBadge() {
* 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'
* }
* ```
*
* @example
* ```tsx
* // Live updates: the badge re-renders when the user changes the permission in
* // the browser UI — no polling, no re-mount.
* function MicIndicator() {
* const { state } = usePermission({ name: "microphone" });
* return <div data-granted={state === "granted"}>Mic: {state ?? "unknown"}</div>;
* }
* ```
*/
export function usePermission(
descriptor: UsePermissionDescriptor
): UsePermissionReturn {
// Every returned field starts deterministic (identical on server and the
// first client render) so the hook never causes a hydration mismatch; the
// effect corrects them after mount. In particular `isSupported` is NOT
// computed in render — that would be `false` on the server and `true` on the
// client's first paint, mismatching any UI that branches on it.
const [state, setState] = useState<PermissionState | null>(null);
const [status, setStatus] = useState<UsePermissionStatus>("idle");
const [error, setError] = useState<Error | null>(null);
const [isSupported, setIsSupported] = useState(false);
// Read the latest descriptor from a ref inside the effect. The effect is keyed
// on the serialized descriptor (a primitive), so it re-runs on content changes
// while `descriptorRef` guarantees the query uses the current fields.
const descriptorRef = useRef(descriptor);
descriptorRef.current = descriptor;
const serialized = serializeDescriptor(descriptor);
useEffect(() => {
const supported = isPermissionsSupported();
setIsSupported(supported);
if (!supported) {
setState(null);
setStatus("unsupported");
setError(null);
return;
}
let cancelled = false;
let permissionStatus: PermissionStatus | null = null;
let changeHandler: (() => void) | null = null;
// Clear any prior permission's value so consumers don't read stale `state`
// while the new query for a changed descriptor is in flight.
setState(null);
setStatus("pending");
setError(null);
navigator.permissions
.query(descriptorRef.current as PermissionDescriptor)
.then((result) => {
if (cancelled) return;
permissionStatus = result;
setState(result.state);
setStatus(result.state);
changeHandler = () => {
// Read the fresh value off the live PermissionStatus object.
setState(result.state);
setStatus(result.state);
};
result.addEventListener("change", changeHandler);
})
.catch((err: unknown) => {
if (cancelled) return;
setState(null);
setStatus("error");
setError(err instanceof Error ? err : new Error(String(err)));
});
return () => {
cancelled = true;
if (permissionStatus && changeHandler) {
permissionStatus.removeEventListener("change", changeHandler);
}
};
// `serialized` is the stable primitive key; `descriptorRef`/setters are stable.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [serialized]);
return useMemo<UsePermissionReturn>(
() => ({ state, status, isSupported, error }),
[state, status, isSupported, error]
);
}
|