The useNotification hook exposes the browser's current notification permission together with explicit operations for requesting permission and displaying a notification. You will wrap the Notifications API without touching browser globals during the first render. The hook must keep permission requests under caller control because browsers expect them to follow a user interaction such as a button click. It must also distinguish an unsupported environment from the browser's three permission values.
type NotificationPermissionState =
| 'default'
| 'granted'
| 'denied'
| 'unsupported';
function useNotification(): {
isSupported: boolean;
permission: NotificationPermissionState;
requestPermission: () => Promise<'default' | 'granted' | 'denied'>;
showNotification: (
title: string,
options?: NotificationOptions
) => Notification;
};
const notifications = useNotification();
// In a supporting browser whose user has not decided yet:
notifications.isSupported; // true
notifications.permission; // 'default'
// Call from a click handler, not during render or mount.
await notifications.requestPermission(); // 'granted'
const notice = notifications.showNotification('Build complete', {
body: 'Version 2.4 is ready.',
});
// notice is the exact Notification instance created by the browser.
If permission is still 'default' or has become 'denied', showNotification() throws instead of calling the constructor. If the browser rejects the permission request or the constructor itself fails, preserve that original error for the caller.
false and 'unsupported'; after mount, reconcile from window.Notification.permission.requestPermission() may call the browser prompt, and the caller is responsible for invoking it from a user interaction.showNotification() must check the browser's current static Notification.permission, not a possibly stale React snapshot.requestPermission and showNotification keep the same identity across rerenders.requestPermission() and the exact instance from showNotification().Keep rendering safe, then cross the browser boundary only from an effect or an explicit callback.
A page can read notification permission, ask the user for it, and create a system notification. React may also render where window does not exist, though, and browsers restrict when a permission prompt may open. Your hook therefore needs a safe initial snapshot and caller-controlled operations.
Treat the browser API as a guarded boundary. The render starts with an environment-safe result, the mount effect reads the real browser state, and later method calls check the API again instead of trusting an old render.
The tempting version reads the global immediately and asks for permission as soon as the component mounts.
function useNotification() {
const [permission, setPermission] = useState(Notification.permission);
useEffect(() => {
Notification.requestPermission().then(setPermission);
}, []);
return { permission };
}
This crashes in an unsupported or server-rendered environment. It also opens a permission prompt without a user interaction, which browsers may block and users do not expect. Finally, it offers no guarded way to construct a notification.
const { useState, useEffect, useCallback } = require('react');
function getNotificationApi() {
if (typeof window === 'undefined' || typeof window.Notification !== 'function') {
return undefined;
}
return window.Notification;
}
function useNotification() {
const [state, setState] = useState({
isSupported: false,
permission: 'unsupported',
});
useEffect(() => {
const NotificationApi = getNotificationApi();
if (!NotificationApi) return;
setState({
isSupported: true,
permission: NotificationApi.permission,
});
}, []);
const requestPermission = useCallback(async () => {
const NotificationApi = getNotificationApi();
if (!NotificationApi) {
throw new Error('Notifications are not supported in this environment.');
}
if (typeof NotificationApi.requestPermission !== 'function') {
throw new Error(
'Notification permission requests are not supported in this environment.'
);
}
const nextPermission = await NotificationApi.requestPermission();
setState({ isSupported: true, permission: nextPermission });
return nextPermission;
}, []);
const showNotification = useCallback((title, options) => {
const NotificationApi = getNotificationApi();
if (!NotificationApi) {
throw new Error('Notifications are not supported in this environment.');
}
if (NotificationApi.permission !== 'granted') {
throw new Error('Notification permission has not been granted.');
}
return new NotificationApi(title, options);
}, []);
return {
...state,
requestPermission,
showNotification,
};
}
module.exports = { useNotification };
getNotificationApi centralizes the environment check, so every browser access has the same guard. The state begins with the safe unsupported snapshot and is reconciled in useEffect, which only runs after mounting. Both callbacks use empty dependency arrays because they read the live browser API when called; they do not close over React's permission snapshot.
Permission requests have three ordinary outcomes, and a browser rejection remains a rejection:
Displaying has a separate call-time gate. Even if the last React render said granted, a user can change site settings before the next call.
The await occurs before setState, so a rejected permission promise cannot invent a new permission. Returning the constructor result gives callers the real Notification instance, including its events and close() method. Constructor failures also propagate without being replaced by a generic hook error.
Suppose the browser supports notifications and its current permission is 'default'. The first render returns { isSupported: false, permission: 'unsupported' } without reading window; after mount, the effect reads the API and updates that to { isSupported: true, permission: 'default' }. The user clicks an Enable button, and the click handler awaits requestPermission(). If the browser resolves 'granted', the hook stores and returns 'granted'. A later call to showNotification('Build complete', { body: 'Version 2.4 is ready.' }) checks the live static permission, constructs the notification with those exact arguments, and returns that instance.
Notification.permission at call time.'denied' hides the real failure; update state only after the promise resolves.new Notification() is suitable for desktop but throws on most mobile browsers; mobile persistent notifications use ServiceWorkerRegistration.showNotification().useCallback keeps both public operations stable.click, show, error, and close events without changing the permission boundary.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
The useNotification hook exposes the browser's current notification permission together with explicit operations for requesting permission and displaying a notification. You will wrap the Notifications API without touching browser globals during the first render. The hook must keep permission requests under caller control because browsers expect them to follow a user interaction such as a button click. It must also distinguish an unsupported environment from the browser's three permission values.
type NotificationPermissionState =
| 'default'
| 'granted'
| 'denied'
| 'unsupported';
function useNotification(): {
isSupported: boolean;
permission: NotificationPermissionState;
requestPermission: () => Promise<'default' | 'granted' | 'denied'>;
showNotification: (
title: string,
options?: NotificationOptions
) => Notification;
};
const notifications = useNotification();
// In a supporting browser whose user has not decided yet:
notifications.isSupported; // true
notifications.permission; // 'default'
// Call from a click handler, not during render or mount.
await notifications.requestPermission(); // 'granted'
const notice = notifications.showNotification('Build complete', {
body: 'Version 2.4 is ready.',
});
// notice is the exact Notification instance created by the browser.
If permission is still 'default' or has become 'denied', showNotification() throws instead of calling the constructor. If the browser rejects the permission request or the constructor itself fails, preserve that original error for the caller.
false and 'unsupported'; after mount, reconcile from window.Notification.permission.requestPermission() may call the browser prompt, and the caller is responsible for invoking it from a user interaction.showNotification() must check the browser's current static Notification.permission, not a possibly stale React snapshot.requestPermission and showNotification keep the same identity across rerenders.requestPermission() and the exact instance from showNotification().Keep rendering safe, then cross the browser boundary only from an effect or an explicit callback.
A page can read notification permission, ask the user for it, and create a system notification. React may also render where window does not exist, though, and browsers restrict when a permission prompt may open. Your hook therefore needs a safe initial snapshot and caller-controlled operations.
Treat the browser API as a guarded boundary. The render starts with an environment-safe result, the mount effect reads the real browser state, and later method calls check the API again instead of trusting an old render.
The tempting version reads the global immediately and asks for permission as soon as the component mounts.
function useNotification() {
const [permission, setPermission] = useState(Notification.permission);
useEffect(() => {
Notification.requestPermission().then(setPermission);
}, []);
return { permission };
}
This crashes in an unsupported or server-rendered environment. It also opens a permission prompt without a user interaction, which browsers may block and users do not expect. Finally, it offers no guarded way to construct a notification.
const { useState, useEffect, useCallback } = require('react');
function getNotificationApi() {
if (typeof window === 'undefined' || typeof window.Notification !== 'function') {
return undefined;
}
return window.Notification;
}
function useNotification() {
const [state, setState] = useState({
isSupported: false,
permission: 'unsupported',
});
useEffect(() => {
const NotificationApi = getNotificationApi();
if (!NotificationApi) return;
setState({
isSupported: true,
permission: NotificationApi.permission,
});
}, []);
const requestPermission = useCallback(async () => {
const NotificationApi = getNotificationApi();
if (!NotificationApi) {
throw new Error('Notifications are not supported in this environment.');
}
if (typeof NotificationApi.requestPermission !== 'function') {
throw new Error(
'Notification permission requests are not supported in this environment.'
);
}
const nextPermission = await NotificationApi.requestPermission();
setState({ isSupported: true, permission: nextPermission });
return nextPermission;
}, []);
const showNotification = useCallback((title, options) => {
const NotificationApi = getNotificationApi();
if (!NotificationApi) {
throw new Error('Notifications are not supported in this environment.');
}
if (NotificationApi.permission !== 'granted') {
throw new Error('Notification permission has not been granted.');
}
return new NotificationApi(title, options);
}, []);
return {
...state,
requestPermission,
showNotification,
};
}
module.exports = { useNotification };
getNotificationApi centralizes the environment check, so every browser access has the same guard. The state begins with the safe unsupported snapshot and is reconciled in useEffect, which only runs after mounting. Both callbacks use empty dependency arrays because they read the live browser API when called; they do not close over React's permission snapshot.
Permission requests have three ordinary outcomes, and a browser rejection remains a rejection:
Displaying has a separate call-time gate. Even if the last React render said granted, a user can change site settings before the next call.
The await occurs before setState, so a rejected permission promise cannot invent a new permission. Returning the constructor result gives callers the real Notification instance, including its events and close() method. Constructor failures also propagate without being replaced by a generic hook error.
Suppose the browser supports notifications and its current permission is 'default'. The first render returns { isSupported: false, permission: 'unsupported' } without reading window; after mount, the effect reads the API and updates that to { isSupported: true, permission: 'default' }. The user clicks an Enable button, and the click handler awaits requestPermission(). If the browser resolves 'granted', the hook stores and returns 'granted'. A later call to showNotification('Build complete', { body: 'Version 2.4 is ready.' }) checks the live static permission, constructs the notification with those exact arguments, and returns that instance.
Notification.permission at call time.'denied' hides the real failure; update state only after the promise resolves.new Notification() is suitable for desktop but throws on most mobile browsers; mobile persistent notifications use ServiceWorkerRegistration.showNotification().useCallback keeps both public operations stable.click, show, error, and close events without changing the permission boundary.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.