Composing Events, Composing Refs
Tags: react
Published: 2024-01-23 04:31:46

#### Polymorphic types
If you are following me along, you may have noticed some bugs.
```jsx
      <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](https://github.com/HariBhandari07/headless-accordion/commit/754d0d7ac0f48366ebdb9b26e70bf6b85a98cf66)

### Composing Events
In `AccordionButton` we have handled the `onClick`, `onKeyDown` events ourselves. What if the users using our component pass their event handlers?
```ts
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`
```jsx
function AccordionButton({ onClick, onKeyDown, ...})
    const handleKeyDown = (e) => {.....}

    const handleClick = (e) => {.....}
    ....
    return (
        <Comp
            onClick={composeEventHandlers(onClick, handleClick)}
            onKeyDown={composeEventHandlers(onKeyDown, handleKeyDown)}
            ...
}
```

Checkpoint [fa99abd82b7ce8c3e10a87b77f40f706a968ede9](https://github.com/HariBhandari07/headless-accordion/commit/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:
```ts
type Ref<T> = RefCallback<T> | RefObject<T> | null;
```
i.e. we can pass a callback function, or ref object from `useRef` in `ref`.

Infact,
```jsx
const inputRef = useRef(null)
....
<input ref={inputRef} />
```
and 
```jsx
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](https://tkdodo.eu/blog/avoiding-use-effect-with-callback-refs) by [Dominik](https://twitter.com/TkDodo)

Assigning/merging multiple refs to a single DOM node will look something like this:
```jsx
// 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:
```ts
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](https://github.com/HariBhandari07/headless-accordion/commit/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](https://turbo.build/repo) or [yarn workspace](https://yarnpkg.com/features/workspaces). 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](https://storybook.js.org/). For testing, I'd use [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/).

I hope this blog series inspired you to explore open-source software. Happy Coding 💻.