Skip to content

combineHooks

combineHooks combines several hooks (or any [get, set] pairs) into one. The combined getter returns the value of the first hook; the combined setter invokes all setters with the same argument.

Sometimes one piece of state is represented in more than one place in the DOM. A classic example is a todo item whose “done” status is both the checked property of its checkbox and a strike-through on its text.

Without combineHooks, keeping the two places in sync is manual:

function Todo({ text }) {
const [checkbox, textSpan] = useRefs();
const [isDone] = useCheckbox(checkbox);
const [, setLineThrough] = useStyleBoolean(
textSpan,
'text-decoration-line',
'line-through',
null,
);
function handleDone(event) {
setLineThrough(event.target.checked);
}
return (
<li>
<input type="checkbox" onChange={handleDone} ref={checkbox} />
<span ref={textSpan}>{text}</span>
</li>
);
}

Every time the state changes, we must remember to touch the second place too. combineHooks lets you declare the relationship instead:

function Todo({ text }) {
const [checkbox, textSpan] = useRefs();
const [isDone, setDone] = combineHooks(
useCheckbox(checkbox),
useStyleBoolean(textSpan, 'text-decoration-line', 'line-through', null),
);
function handleDone(event) {
setDone(event.target.checked);
}
return (
<li>
<input type="checkbox" onChange={handleDone} ref={checkbox} />
<span ref={textSpan}>{text}</span>
</li>
);
}

Now setDone(...) updates the checkbox and the strike-through in one call, and isDone() still reads from the checkbox.

  • The combined getter returns the result of the first hook’s getter.
  • The combined setter invokes every hook’s setter with the same argument.

Because the getter comes from the first hook, put the “source of truth” first. In the example above, the checkbox is declared as the source of truth and the strike-through is declared as a projection of it. When the user toggles the checkbox, the browser updates its checked property natively and the handler propagates the change to the text. When something else calls setDone(false) — say, a “reset all” button — both places are updated.

Hooks can be initialized from props like any other element:

function Todo({ text, done = false }) {
// ...
return (
<li>
<input type="checkbox" checked={done} onChange={handleDone} ref={checkbox} />
<span ref={textSpan}>{text}</span>
</li>
);
}

The checked attribute becomes the initial value of the checkbox, and isDone() reads it back.