Implement an indeterminate checkbox in React with TypeScript

Posted on

This is a follow-up to my previous blog post: Indeterminate checkboxes are weird . Now that we have pulled back the curtain on tri-state checkboxes, we can implement a Checkbox component in React with TypeScript. The benefits of wrapping the native element in a component is that we can support an indeterminate prop right on the component itself.

The goal is to use the Checkbox like so, where checked and indeterminate are two boolean props on the component.

<Checkbox checked indeterminate/>

Here is the full implementation:

import {
DetailedHTMLProps,
forwardRef,
InputHTMLAttributes,
useEffect,
useRef,
} from "react";

interface Props
extends DetailedHTMLProps<
InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>
{
/**
* If `true`, the checkbox is checked. If `false`, the
* checkbox is not checked. If left undefined, the checkbox
* is uncontrolled.
*
* https://reactjs.org/docs/glossary.html#controlled-vs-uncontrolled-components
*/

checked?: boolean;

/**
* If `true`, the checkbox gives an appearance of being
* in an indeterminate state.
*/

indeterminate?: boolean;

/**
* Do not pass in a `type` prop. We force the input
* to be type "checkbox".
*/

type?: never;
}

export const Checkbox = forwardRef<HTMLInputElement, Props>(
({ indeterminate = false, type, ...inputProps }, ref) => {
// We need our own internal ref to ensure that it is
// 1. actually defined, and
// 2. an object ref rather than a callback ref.
const internalRef = useRef<HTMLInputElement | null>(null);

// This function is a callback ref that will keep our internal
// ref and the forwarded parent ref synchronized.
function synchronizeRefs(el: HTMLInputElement | null) {
// Update the internal ref.
internalRef.current = el;

// Update the provided ref.
if (!ref) {
// nothing to update
} else if (typeof ref === "object") {
ref.current = el;
} else {
// must be a callback ref
ref(el);
}
}

// We use an effect here to update the `indeterminate` IDL
// attribute on the input element whenever the prop value changes.
useEffect(() => {
if (internalRef.current) {
internalRef.current.indeterminate = indeterminate;
}
}, [indeterminate]);

return <input ref={synchronizeRefs} type="checkbox" {...inputProps}/>;
}
);

You can play around with it in this Codesandbox.

Happy hacking!