The argument to useState is an ordinary expression, evaluated on every render even though React 7,897 uses it only on the first. For a number that costs nothing. For anything that reads localStorage, parses JSON or builds a large array, you pay the full price on every keystroke and throw the result away.
// createInitialTodos() runs on every render; React keeps only the first result
const [slow, setSlow] = useState(createInitialTodos());
// the function itself is passed, so React calls it on the first render only
const [todos, setTodos] = useState(createInitialTodos);
// the usual shape when the initial value needs arguments or a fallback
const [draft, setDraft] = useState(() => JSON.parse(localStorage.draft ?? '{}'));The difference is one pair of parentheses, which is why it is so easy to get wrong. A quick test: if the expression would be worth wrapping in useMemo, it belongs behind an initializer instead.
An initializer must be pure and takes no arguments. In development, Strict Mode (Strict Mode) calls it twice on purpose, so one that increments a counter or pushes onto a shared array shows up as a bug on your machine rather than in production. Updater functions get the same treatment.