I have a Checkbox react component that has to support the indeterminate state, but I'm updating our components to forward refs properly. The checkbox component already uses a callback ref internally to set the indeterminate property. Here's the original component (simplified):
export type ICheckboxProps = {
checked?: boolean
indeterminate?: boolean
} & React.InputHTMLAttributes<HTMLInputElement>
export const Checkbox: React.FC<ICheckboxProps> = props => {
const { checked = false, ...rest } = props;
return (
<input
type="checkbox"
checked={checked}
ref={ref => {
if (ref) {
ref.indeterminate = !checked ? indeterminate! : false;
}
}}
{...rest}
/>
)
}
Now, since this is a UI library, I'm trying to forward a ref as well. But that clashes with the callback ref - now I have two separate refs. Additionally, the forwarded ref could be a callback ref. So I can't even access the instance there to set the indeterminate property. I've tried a bunch of stuff, but no matter what I do typescript's helpful red underlines tell me that I'm wrong.
How do I both apply the forwarded ref to the input and set the indeterminate property on the input?
Here's it most of the way but there's an issue noted:
export type ICheckboxProps = {
checked?: boolean
indeterminate?: boolean
} & React.InputHTMLAttributes<HTMLInputElement>
export const Checkbox = React.forwardRef<HTMLInputElement, ICheckboxProps>((props, inRef) => {
const { checked = false, ...rest } = props;
return (
<input
type="checkbox"
checked={checked}
ref={ref => {
if (ref) {
ref.indeterminate = !checked ? indeterminate! : false;
if (inRef) {
if (typeof inRef === "function") {
inRef(ref)
} else {
inRef.current = ref // Cannot assign to 'current' because it is a read-only property.
}
}
}
}}
{...rest}
/>
)
})
The useImperativeHandle
hook does almost exactly the same as your second example. The assignment of ref.current
to inRef
is then handled internally by React, and you don't have to break the contract by changing the readonly property.
export const Checkbox = React.forwardRef<HTMLInputElement, ICheckboxProps>((props, inRef) => {
const { checked = false, indeterminate, ...rest } = props;
const ref = useRef<HTMLInputElement>(null)
useImperativeHandle(inRef, () => ref.current!, [ref])
return (
<input
type="checkbox"
checked={checked}
ref={ref}
{...rest}
/>
)
})
In typescript playground
I would like to make a comment on the non-null assertion on ref.current
. As far as I can tell, refs for children are resolved before refs for parents, but the only related statement in the documentation I could find is calling inputRef.current.focus()
without a null guard in the documentation of useImperativeHandle
You can do anything you want with the forwarded ref, including setting its current value:
const Checkbox = React.forwardRef(({ checked = false, indeterminate, ...rest }, forwardedRef) => (
<input
type="checkbox"
checked={checked}
ref={(inputElement) => {
if (inputElement) {
inputElement.indeterminate = !checked && indeterminate
}
if (forwardedRef) {
if(typeof(forwardedRef) === "function") {
forwardedRef(inputElement)
} else {
forwardedRef.current = inputElement
}
}
{...rest}
/>
))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With