DEV Community

Cover image for Composing Events, Composing Refs
Hari Bhandari
Hari Bhandari

Posted on • Updated on

Composing Events, Composing Refs

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>
Enter fullscreen mode Exit fullscreen mode

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);
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

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)}
            ...
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

i.e. we can pass a callback function, or ref object from useRef in ref.

Infact,

const inputRef = useRef(null)
....
<input ref={inputRef} />
Enter fullscreen mode Exit fullscreen mode

and

const inputRef = useRef(null)
...
<input ref={(node) => { inputRef.current = node }} />
Enter fullscreen mode Exit fullscreen mode

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>
    )
})
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)