A screen wake lock asks the device to keep its display awake while a visible page needs the user's attention. Implement a React hook around the Screen Wake Lock API that reports support and active state, exposes stable request and release operations, and follows browser-initiated release. The hook must also handle overlapping requests and component cleanup without leaking a granted lock.
function useWakeLock(): {
isSupported: boolean;
isActive: boolean;
error: Error | null;
request: () => Promise<void>;
release: () => Promise<void>;
}
const { result } = renderHook(() => useWakeLock());
await act(async () => result.current.request());
// navigator.wakeLock.request was called with 'screen'
// result.current.isActive === true
await act(async () => result.current.release());
// The held sentinel was released.
// result.current.isActive === false
isSupported becomes true only when navigator.wakeLock.request is a function; the initial server-safe state is false.isActive to false.Error objects and reject request; a failed manual release stays active and rejects release.screen lock type. Do not add visibility-based reacquisition, permission checks, or battery logic.The hook owns one wake-lock sentinel and treats that sentinel's lifecycle as the source of truth.
A recipe or ticket page may need the screen to stay awake even when the user is not touching it. The browser returns a WakeLockSentinel when it grants that request, but the platform may later release it because the page is hidden or the device enters a power-saving state. Your hook has to follow both your own calls and those external changes.
Think of the sentinel as a revocable lease. The hook becomes active only after a lease arrives, and either a manual release or the sentinel's release event ends that lease.
The tempting version flips state around the two method calls:
async function request() {
await navigator.wakeLock.request('screen');
setIsActive(true);
}
async function release() {
setIsActive(false);
}
This loses the returned sentinel, so it cannot release the native lock or hear a system revocation. Two quick calls also create two native requests, and a request that finishes after unmount can leave the screen lock held by a component that no longer exists.
const { useState, useRef, useEffect, useCallback } = require('react');
function asError(value, fallback) {
if (value instanceof Error) return value;
return new Error(typeof value === 'string' && value ? value : fallback);
}
function useWakeLock() {
const [isSupported, setIsSupported] = useState(false);
const [isActive, setIsActive] = useState(false);
const [error, setError] = useState(null);
const sentinelRef = useRef(null);
const pendingRef = useRef(null);
const mountedRef = useRef(false);
useEffect(() => {
mountedRef.current = true;
const supported =
typeof navigator !== 'undefined' &&
typeof navigator.wakeLock?.request === 'function';
setIsSupported(supported);
return () => {
mountedRef.current = false;
const entry = sentinelRef.current;
sentinelRef.current = null;
if (entry) {
entry.sentinel.removeEventListener('release', entry.onRelease);
Promise.resolve()
.then(() => entry.sentinel.release())
.catch(() => {}); // Cleanup cannot report an error to an unmounted component.
}
};
}, []);
const request = useCallback(() => {
if (sentinelRef.current) return Promise.resolve();
if (pendingRef.current) return pendingRef.current;
setError(null);
const wakeLock =
typeof navigator !== 'undefined' ? navigator.wakeLock : undefined;
if (!wakeLock || typeof wakeLock.request !== 'function') {
const unsupported = new Error('Screen Wake Lock API is not supported');
if (mountedRef.current) setError(unsupported);
return Promise.reject(unsupported);
}
const task = Promise.resolve()
.then(() => wakeLock.request('screen'))
.then(async (sentinel) => {
if (!mountedRef.current) {
try {
await sentinel.release();
} catch {}
return;
}
const entry = { sentinel, onRelease: null };
entry.onRelease = () => {
if (sentinelRef.current !== entry) return;
sentinel.removeEventListener('release', entry.onRelease);
sentinelRef.current = null;
if (mountedRef.current) setIsActive(false);
};
sentinel.addEventListener('release', entry.onRelease);
sentinelRef.current = entry;
setIsActive(true);
})
.catch((reason) => {
const nextError = asError(reason, 'Unable to request a screen wake lock');
if (mountedRef.current) setError(nextError);
throw nextError;
})
.finally(() => {
if (pendingRef.current === task) pendingRef.current = null;
});
pendingRef.current = task;
return task;
}, []);
const release = useCallback(async () => {
const entry = sentinelRef.current;
if (!entry) return;
await entry.sentinel.release();
if (sentinelRef.current === entry) {
entry.sentinel.removeEventListener('release', entry.onRelease);
sentinelRef.current = null;
if (mountedRef.current) setIsActive(false);
}
}, []);
return { isSupported, isActive, error, request, release };
}
module.exports = { useWakeLock };
Refs hold mutable resources without causing a render. pendingRef gives every overlapping caller the same promise, while sentinelRef prevents another request after the lock becomes active. The release listener checks that it still belongs to the current sentinel before changing state, which prevents an old event from clearing a newer lock.
Suppose three components call request() before the browser responds. The first call stores a pending promise; calls two and three return that same promise without touching the native API. When the browser grants sentinel A, the hook attaches one listener, stores A, and renders isActive: true. If the system later revokes A, its event removes that exact listener, clears A, and renders isActive: false.
Unmount adds one more race. The cleanup marks the hook unmounted before releasing a held sentinel. If the native request is still pending, its eventual sentinel is released immediately and is never stored or subscribed.
release() call.visibilitychange makes the document visible, if the product still needs the lock.screen-wake-lock Permissions Policy when the hook is used inside an iframe.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
A screen wake lock asks the device to keep its display awake while a visible page needs the user's attention. Implement a React hook around the Screen Wake Lock API that reports support and active state, exposes stable request and release operations, and follows browser-initiated release. The hook must also handle overlapping requests and component cleanup without leaking a granted lock.
function useWakeLock(): {
isSupported: boolean;
isActive: boolean;
error: Error | null;
request: () => Promise<void>;
release: () => Promise<void>;
}
const { result } = renderHook(() => useWakeLock());
await act(async () => result.current.request());
// navigator.wakeLock.request was called with 'screen'
// result.current.isActive === true
await act(async () => result.current.release());
// The held sentinel was released.
// result.current.isActive === false
isSupported becomes true only when navigator.wakeLock.request is a function; the initial server-safe state is false.isActive to false.Error objects and reject request; a failed manual release stays active and rejects release.screen lock type. Do not add visibility-based reacquisition, permission checks, or battery logic.The hook owns one wake-lock sentinel and treats that sentinel's lifecycle as the source of truth.
A recipe or ticket page may need the screen to stay awake even when the user is not touching it. The browser returns a WakeLockSentinel when it grants that request, but the platform may later release it because the page is hidden or the device enters a power-saving state. Your hook has to follow both your own calls and those external changes.
Think of the sentinel as a revocable lease. The hook becomes active only after a lease arrives, and either a manual release or the sentinel's release event ends that lease.
The tempting version flips state around the two method calls:
async function request() {
await navigator.wakeLock.request('screen');
setIsActive(true);
}
async function release() {
setIsActive(false);
}
This loses the returned sentinel, so it cannot release the native lock or hear a system revocation. Two quick calls also create two native requests, and a request that finishes after unmount can leave the screen lock held by a component that no longer exists.
const { useState, useRef, useEffect, useCallback } = require('react');
function asError(value, fallback) {
if (value instanceof Error) return value;
return new Error(typeof value === 'string' && value ? value : fallback);
}
function useWakeLock() {
const [isSupported, setIsSupported] = useState(false);
const [isActive, setIsActive] = useState(false);
const [error, setError] = useState(null);
const sentinelRef = useRef(null);
const pendingRef = useRef(null);
const mountedRef = useRef(false);
useEffect(() => {
mountedRef.current = true;
const supported =
typeof navigator !== 'undefined' &&
typeof navigator.wakeLock?.request === 'function';
setIsSupported(supported);
return () => {
mountedRef.current = false;
const entry = sentinelRef.current;
sentinelRef.current = null;
if (entry) {
entry.sentinel.removeEventListener('release', entry.onRelease);
Promise.resolve()
.then(() => entry.sentinel.release())
.catch(() => {}); // Cleanup cannot report an error to an unmounted component.
}
};
}, []);
const request = useCallback(() => {
if (sentinelRef.current) return Promise.resolve();
if (pendingRef.current) return pendingRef.current;
setError(null);
const wakeLock =
typeof navigator !== 'undefined' ? navigator.wakeLock : undefined;
if (!wakeLock || typeof wakeLock.request !== 'function') {
const unsupported = new Error('Screen Wake Lock API is not supported');
if (mountedRef.current) setError(unsupported);
return Promise.reject(unsupported);
}
const task = Promise.resolve()
.then(() => wakeLock.request('screen'))
.then(async (sentinel) => {
if (!mountedRef.current) {
try {
await sentinel.release();
} catch {}
return;
}
const entry = { sentinel, onRelease: null };
entry.onRelease = () => {
if (sentinelRef.current !== entry) return;
sentinel.removeEventListener('release', entry.onRelease);
sentinelRef.current = null;
if (mountedRef.current) setIsActive(false);
};
sentinel.addEventListener('release', entry.onRelease);
sentinelRef.current = entry;
setIsActive(true);
})
.catch((reason) => {
const nextError = asError(reason, 'Unable to request a screen wake lock');
if (mountedRef.current) setError(nextError);
throw nextError;
})
.finally(() => {
if (pendingRef.current === task) pendingRef.current = null;
});
pendingRef.current = task;
return task;
}, []);
const release = useCallback(async () => {
const entry = sentinelRef.current;
if (!entry) return;
await entry.sentinel.release();
if (sentinelRef.current === entry) {
entry.sentinel.removeEventListener('release', entry.onRelease);
sentinelRef.current = null;
if (mountedRef.current) setIsActive(false);
}
}, []);
return { isSupported, isActive, error, request, release };
}
module.exports = { useWakeLock };
Refs hold mutable resources without causing a render. pendingRef gives every overlapping caller the same promise, while sentinelRef prevents another request after the lock becomes active. The release listener checks that it still belongs to the current sentinel before changing state, which prevents an old event from clearing a newer lock.
Suppose three components call request() before the browser responds. The first call stores a pending promise; calls two and three return that same promise without touching the native API. When the browser grants sentinel A, the hook attaches one listener, stores A, and renders isActive: true. If the system later revokes A, its event removes that exact listener, clears A, and renders isActive: false.
Unmount adds one more race. The cleanup marks the hook unmounted before releasing a held sentinel. If the native request is still pending, its eventual sentinel is released immediately and is never stored or subscribed.
release() call.visibilitychange makes the document visible, if the product still needs the lock.screen-wake-lock Permissions Policy when the hook is used inside an iframe.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.