Build a custom React hook that works exactly like useState, with one twist: whenever the stored state is null or undefined, reading the value gives you a fallback default instead. It's the hook you reach for when a value might legitimately go missing — a form field that hasn't loaded, an API response that came back empty — and you'd rather render a sensible placeholder than undefined. The catch is that "missing" must mean only null or undefined, never a real value that happens to be falsy.
function useDefault<T>(
defaultValue: T,
initialValue: T | null | undefined
): [T, (next: T | null | undefined) => void];
useDefault returns a [value, setValue] pair. setValue behaves like a normal state setter. When the stored state is null or undefined, value reads as defaultValue.
function Greeting({ name }) {
const [displayName, setDisplayName] = useDefault('Guest', name);
return <h1>Hello, {displayName}</h1>;
}
// name = 'Ada' → "Hello, Ada"
// name = undefined → "Hello, Guest"
// name = null → "Hello, Guest"
// Falsy-but-real values must survive — they are NOT replaced:
useDefault('fallback', 0); // value === 0
useDefault('fallback', ''); // value === ''
useDefault('fallback', false); // value === false
// Setting the state to null or undefined brings the default back:
const [value, setValue] = useDefault('fallback', 'hi');
setValue(null); // value === 'fallback'
null and undefined trigger the fallback. 0, '', and false are real values and must pass through unchanged. This is the whole point of the hook.setValue is a plain state setter. It accepts a value and updates the state, just like the setter from useState. Supporting a functional updater is a nice-to-have, not a requirement.null. You only substitute the default when handing the value back.