A preferred-language hook reports the browser's first language choice and stays current when that preference changes. Browsers expose the ordered list through navigator.languages, with navigator.language as the single-language fallback. Your hook turns those browser values into React state without assuming that browser globals exist during the first render.
function usePreferredLanguage(defaultValue?: string): string;
defaultValue is 'en-US' when omitted. It is the environment-safe value until a supporting browser becomes authoritative after mount.
// navigator.languages is ['fr-CA', 'fr', 'en']
renderHook(() => usePreferredLanguage()).result.current; // 'fr-CA'
// navigator.languages is [], navigator.language is 'de-DE'
renderHook(() => usePreferredLanguage('en-US')).result.current; // 'de-DE'
// If both browser values are unavailable:
renderHook(() => usePreferredLanguage('es-MX')).result.current; // 'es-MX'
navigator.languages[0] when it is a non-empty string, then navigator.language, then defaultValue.languagechange event and read both navigator properties when it fires.defaultValue changes on rerender, the stable event handler must use that new value when browser preferences later become unavailable.document.documentElement.lang, parse language tags, or persist an override.You'll mirror the browser's primary language preference into React state, while keeping the first render safe for environments that have no browser APIs.
Your application may choose translated copy, number formats, or localized routes from the visitor's preferred language. The browser exposes an ordered language list, but that preference can change while the page remains open. The hook therefore needs a safe fallback, one browser read after mount, and a subscription that repeats the read after each languagechange event.
Think of the browser values as a short priority ladder. The first navigator.languages entry wins when it is a non-empty string. Otherwise the hook tries navigator.language, then the caller's fallback.
The tempting version reads the browser inside the state initializer and never listens for changes:
const { useState } = require('react');
function usePreferredLanguage(defaultValue = 'en-US') {
const [language] = useState(
navigator.languages[0] || navigator.language || defaultValue,
);
return language;
}
This can throw when navigator is unavailable during server rendering. It also captures only the mount-time value. A later browser preference change cannot update React because there is no listener and no state setter.
const { useEffect, useRef, useState } = require('react');
function isNonEmptyString(value) {
return typeof value === 'string' && value.length > 0;
}
function readPreferredLanguage(fallback) {
if (typeof navigator === 'undefined') return fallback;
// Read both properties now so every event observes the browser's latest snapshot.
const languages = navigator.languages;
const language = navigator.language;
const primary = Array.isArray(languages) ? languages[0] : undefined;
if (isNonEmptyString(primary)) return primary;
if (isNonEmptyString(language)) return language;
return fallback;
}
function usePreferredLanguage(defaultValue = 'en-US') {
const fallbackRef = useRef(defaultValue);
fallbackRef.current = defaultValue;
const [preferredLanguage, setPreferredLanguage] = useState(defaultValue);
useEffect(() => {
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
return undefined;
}
const handleLanguageChange = () => {
setPreferredLanguage(readPreferredLanguage(fallbackRef.current));
};
window.addEventListener('languagechange', handleLanguageChange);
// Subscribe before reconciling so no change can slip between the read and add.
handleLanguageChange();
return () => window.removeEventListener('languagechange', handleLanguageChange);
}, []);
return preferredLanguage;
}
module.exports = { usePreferredLanguage };
State begins with defaultValue, so rendering does not require browser globals. The mount-only effect owns one stable listener and reconciles immediately from the current browser snapshot. fallbackRef lets that listener see a fallback supplied by a later render without making the effect subscribe again.
Suppose defaultValue is 'en-US', while the browser currently reports navigator.languages = ['fr-CA', 'fr'] and navigator.language = 'fr-CA':
'en-US' without reading window or navigator.handleLanguageChange once on window before its first browser read.'fr-CA', so React state becomes 'fr-CA'.['de-DE', 'en-US'] and dispatches languagechange.'de-DE'; it does not reuse 'fr-CA' from mount.removeEventListener.Mounting and each language change take O(1) time and the hook stores O(1) state. Each mounted hook instance owns one window listener.
navigator.languages in the state initializer can fail outside a browser and can make the first client render disagree with server output. Fix: initialize from the fallback and reconcile in an effect.languagechange event says that preferences changed; its useful values still live on navigator. Fix: read both properties inside the handler every time.navigator.languages[0]; selecting a later entry changes the browser's stated order. Fix: try the first entry, then the single-language property.defaultValue to the effect dependencies replaces the listener on every fallback change. Fix: keep the latest fallback in a ref that the stable handler reads.removeEventListener does not remove the registered function. Fix: add and remove the same named handler from the effect closure.navigator.languages array when language negotiation needs more than the primary choice.lang attribute; keep that side effect outside this read-only hook.useSyncExternalStore could let many consumers reuse one window listener while preserving snapshot semantics.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
A preferred-language hook reports the browser's first language choice and stays current when that preference changes. Browsers expose the ordered list through navigator.languages, with navigator.language as the single-language fallback. Your hook turns those browser values into React state without assuming that browser globals exist during the first render.
function usePreferredLanguage(defaultValue?: string): string;
defaultValue is 'en-US' when omitted. It is the environment-safe value until a supporting browser becomes authoritative after mount.
// navigator.languages is ['fr-CA', 'fr', 'en']
renderHook(() => usePreferredLanguage()).result.current; // 'fr-CA'
// navigator.languages is [], navigator.language is 'de-DE'
renderHook(() => usePreferredLanguage('en-US')).result.current; // 'de-DE'
// If both browser values are unavailable:
renderHook(() => usePreferredLanguage('es-MX')).result.current; // 'es-MX'
navigator.languages[0] when it is a non-empty string, then navigator.language, then defaultValue.languagechange event and read both navigator properties when it fires.defaultValue changes on rerender, the stable event handler must use that new value when browser preferences later become unavailable.document.documentElement.lang, parse language tags, or persist an override.You'll mirror the browser's primary language preference into React state, while keeping the first render safe for environments that have no browser APIs.
Your application may choose translated copy, number formats, or localized routes from the visitor's preferred language. The browser exposes an ordered language list, but that preference can change while the page remains open. The hook therefore needs a safe fallback, one browser read after mount, and a subscription that repeats the read after each languagechange event.
Think of the browser values as a short priority ladder. The first navigator.languages entry wins when it is a non-empty string. Otherwise the hook tries navigator.language, then the caller's fallback.
The tempting version reads the browser inside the state initializer and never listens for changes:
const { useState } = require('react');
function usePreferredLanguage(defaultValue = 'en-US') {
const [language] = useState(
navigator.languages[0] || navigator.language || defaultValue,
);
return language;
}
This can throw when navigator is unavailable during server rendering. It also captures only the mount-time value. A later browser preference change cannot update React because there is no listener and no state setter.
const { useEffect, useRef, useState } = require('react');
function isNonEmptyString(value) {
return typeof value === 'string' && value.length > 0;
}
function readPreferredLanguage(fallback) {
if (typeof navigator === 'undefined') return fallback;
// Read both properties now so every event observes the browser's latest snapshot.
const languages = navigator.languages;
const language = navigator.language;
const primary = Array.isArray(languages) ? languages[0] : undefined;
if (isNonEmptyString(primary)) return primary;
if (isNonEmptyString(language)) return language;
return fallback;
}
function usePreferredLanguage(defaultValue = 'en-US') {
const fallbackRef = useRef(defaultValue);
fallbackRef.current = defaultValue;
const [preferredLanguage, setPreferredLanguage] = useState(defaultValue);
useEffect(() => {
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
return undefined;
}
const handleLanguageChange = () => {
setPreferredLanguage(readPreferredLanguage(fallbackRef.current));
};
window.addEventListener('languagechange', handleLanguageChange);
// Subscribe before reconciling so no change can slip between the read and add.
handleLanguageChange();
return () => window.removeEventListener('languagechange', handleLanguageChange);
}, []);
return preferredLanguage;
}
module.exports = { usePreferredLanguage };
State begins with defaultValue, so rendering does not require browser globals. The mount-only effect owns one stable listener and reconciles immediately from the current browser snapshot. fallbackRef lets that listener see a fallback supplied by a later render without making the effect subscribe again.
Suppose defaultValue is 'en-US', while the browser currently reports navigator.languages = ['fr-CA', 'fr'] and navigator.language = 'fr-CA':
'en-US' without reading window or navigator.handleLanguageChange once on window before its first browser read.'fr-CA', so React state becomes 'fr-CA'.['de-DE', 'en-US'] and dispatches languagechange.'de-DE'; it does not reuse 'fr-CA' from mount.removeEventListener.Mounting and each language change take O(1) time and the hook stores O(1) state. Each mounted hook instance owns one window listener.
navigator.languages in the state initializer can fail outside a browser and can make the first client render disagree with server output. Fix: initialize from the fallback and reconcile in an effect.languagechange event says that preferences changed; its useful values still live on navigator. Fix: read both properties inside the handler every time.navigator.languages[0]; selecting a later entry changes the browser's stated order. Fix: try the first entry, then the single-language property.defaultValue to the effect dependencies replaces the listener on every fallback change. Fix: keep the latest fallback in a ref that the stable handler reads.removeEventListener does not remove the registered function. Fix: add and remove the same named handler from the effect closure.navigator.languages array when language negotiation needs more than the primary choice.lang attribute; keep that side effect outside this read-only hook.useSyncExternalStore could let many consumers reuse one window listener while preserving snapshot semantics.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.