Polymorphic types
If you are following me along, you may have noticed some bugs.
<Accordion onClick={() =>{}} className='someClass'>
<Accordion.Item onBlur={() => {}}>
<Accordion.Button onClick={() => {}}>Button 1</Accordion.Button>
<Accordion.Panel>Panel 1</Accordion.Panel>
</Accordion.Item>
Here, even though our Accordion
, Accordion.Item
, Accordion.Button
are essentially HTML
elements, we will get type errors when we pass HTML attributes and synthetic event props, as shown above. Hardcoding all the HTML attributes and synthetic events as props is not the Typescript way. What we want here is a way to merge the props defined by us with the HTML-derived attributes/props.
For this typescript wizardry, I copied types from reach-ui/polymorphic
package.
Checkpoint: 754d0d7ac0f48366ebdb9b26e70bf6b85a98cf66
Composing Events
In AccordionButton
we have handled the onClick
, onKeyDown
events ourselves. What if the users using our component pass their event handlers?
export function composeEventHandlers<E>(
theirEventHandler?: (event: E) => void,
ourEventHandler?: (event: E) => void
) {
return function handleEvent(event: E) {
theirEventHandler?.(event);
if (!(event as unknown as Event).defaultPrevented) {
return ourEventHandler?.(event);
}
};
}
If the users provide their event handler, we call that event handler first and then call our event handler. If a user wishes to prevent our event handler from firing, they can simply do event.preventDefault()
in their event handler.
Now, wrap the event handlers with composeEventHandler
function AccordionButton({ onClick, onKeyDown, ...})
const handleKeyDown = (e) => {.....}
const handleClick = (e) => {.....}
....
return (
<Comp
onClick={composeEventHandlers(onClick, handleClick)}
onKeyDown={composeEventHandlers(onKeyDown, handleKeyDown)}
...
}
Checkpoint fa99abd82b7ce8c3e10a87b77f40f706a968ede9
Composing Refs
In the Keyboard navigation section, I discarded the forwardedRef
and used internal ref to maintain focus on the active accordion button. We need to handle the refs provided by users and the refs that we create.
If you check the type signature of ref
, you can see something like this:
type Ref<T> = RefCallback<T> | RefObject<T> | null;
i.e. we can pass a callback function, or ref object from useRef
in ref
.
Infact,
const inputRef = useRef(null)
....
<input ref={inputRef} />
and
const inputRef = useRef(null)
...
<input ref={(node) => { inputRef.current = node }} />
are the same. All ref props are just functions. For more info regarding callback refs, check this nice blog post by Dominik
Assigning/merging multiple refs to a single DOM node will look something like this:
// Contrived example
const = DummyComponent({props}, forwadedRef) => {
const ref1 = useRef(null)
const ref2 = (node) => console.log(node)
...
return (
<h2
ref={(node) => {
ref1.current = node;
ref2(node);
forwardedRef.current = node;
}}
>
Dummy Component
</ h2>
)
})
If we extract this into a custom hook it will look something like this:
export function useComposedRefs<T>(...refs: PossibleRefs<T>[]) {
return React.useCallback((node: T) => {
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(node);
} else if (ref != null) {
(ref as React.MutableRefObject<T>).current = node;
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, refs);
}
Checkpoint fbcccd29838e4e3c468f407c0c17c68c0285ac25
The End
Our Accordion is now complete.
If I were building a component library, it would be a mono repo, and I would use tools like turbo repo or yarn workspace. Each component would be a package, so if I need to use a component in some project I would have to install just the component rather than the entire library. Notably, Reach UI uses Turbo repo and Radix UI uses yarn workspace.
For documenting component variants, I would use Storybook. For testing, I'd use Jest and React Testing Library.
I hope this blog series inspired you to explore open-source software. Happy Coding 💻.
Top comments (0)