Descendants pattern
Tags: react
Published: 2024-01-23 04:28:46

Up until now, we have been passing `index` in `AccordionItem` to keep track of each accordion item (children/descendants). It's time to get rid of them. 

We need to find a way to get and keep track of each `AccordionItem` index in the `Accordion` component.

```jsx
  const index = useRef(-1);
  ...
  <div ref={() => {
      index.current++;
      console.log(index.current, "first");
    }}>
    First
  </div>
  <div ref={() => {
      index.current++;
      console.log(index.current, "second");
    }}>
    Second
  </div>
  <div ref={() => {
      index.current++;
      console.log(index.current, "third");
    }}>
    Third
  </div>
```
If you check console, it will be
```js
0 'first'
1 'second'
2 'third'
```
Here, we have an `index` counter, and with each `div` render we are increasing the `index` counter. If you check the console and correlate it with the corresponding `div`, the first div has an index of 0, the second div an index of 1, and the third div an index of 2.

We can apply the same logic to keep track of each `AccordionItem` index. By having an index counter in `Accordion` and incrementing the index every time  `AccordionItem` is called/rendered.

```jsx
const Accordion = forwardRef(function (
 ....
) {
  ....
  const indexCounter = useRef(-1); // descendants counter
  const descendantsMap = useRef<Record<string, number>>({}); // keep track of descends in format { 'id1': 0, 'id2': 1,...}

  const getIndex = useCallback((id: string) => {
    if (!descendantsMap.current[id]) {
      descendantsMap.current[id] = ++indexCounter.current;
    }
    return descendantsMap.current[id];
  }, []);

  const context = {
    ....
    getIndex,
  };

  return (
    <AccordionContext.Provider value={context}>
     ...
    </AccordionContext.Provider>
  );
});

const AccordionItem = forwardRef(function (
  ...
) {
  const { openPanels, getIndex } = useAccordionContext();

  const itemId = useRef<string>(useId());
  const [index, setIndex] = useState(-1);

  useLayoutEffect(() => {
    setIndex(getIndex(itemId.current));
  }, [getIndex]);

  .....
});
```
Here,
- When an `AccordionItem` is mounted, it's assigned a unique ID and index of -1.
- Subsequently, `AccordionItem` registers itself with `getIndex`. At this point, `descendantsMap` retrieves the ID of the `AccordionItem`, records that ID, and returns the index.
- With each call to `getIndex` (with each render of `AccordionItem`), `indexCounter` increments by 1.
- In this way `descendantMap` of `Accordion` will keep track of each `AccordionItem` and its index.

Checkpoint: [50c3a0b5f8d7c880b11f30080531b60aba8178ec](https://github.com/HariBhandari07/headless-accordion/commit/50c3a0b5f8d7c880b11f30080531b60aba8178ec)

The descendant pattern can be applied to other components as well. So, we will extract it into separate hooks and providers.
```jsx
const DescendantContext = createContext<DescendantProviderProps>(
  {} as DescendantProviderProps
);

export const Descendants = ({
  children,
  value,
}: {
  children: ReactNode;
  value: DescendantProviderProps;
}) => {
  value.reset();

  return (
    <DescendantContext.Provider value={value}>
      {children}
    </DescendantContext.Provider>
  );
};

export const useDescendants = () => {
  const indexCounter = useRef(0);
  const map = useRef<Record<string, any>>({});

  const getIndex = useCallback((id: string, props?: IgetIndexProps) => {
    const hidden = props ? props.hidden : false;
    if (!map.current[id]) {
      map.current[id] = { index: hidden ? -1 : indexCounter.current++ };
    }
    map.current[id].props = props;
    return map.current[id].index;
  }, []);

  // reset the counter and map
  const reset = useCallback(() => {
    indexCounter.current = 0;
    map.current = {};
  }, []);

  return { getIndex, map, reset };
};

export const useDescendant = (props?: Record<string, any>) => {
  const context = useContext(DescendantContext);
  const descendantId = useRef<string>();
  if (!descendantId.current) {
    descendantId.current = randomId();
  }

  const [index, setIndex] = useState(-1);

  useIsomorphicLayoutEffect(() => {
    setIndex(context?.getIndex(descendantId.current as string, props));
  }, []);

  return index;
};
```
Here, the logic implemented in `Accordion` and `AccordionItem` is kept inside the `useDescendants` hook and `useDescendant` hook, respectively. Additionally, a separate context for descendants is created. Here, we have created an open-source [use-descendants](https://github.com/pacocoursey/use-descendants) hook library.

Checkpoint: [9dd86997b99951fa035be73230f8381d3ea189ba](https://github.com/HariBhandari07/headless-accordion/commit/9dd86997b99951fa035be73230f8381d3ea189ba)