DEV Community

Cover image for Descendants pattern
Hari Bhandari
Hari Bhandari

Posted on • Updated on

Descendants pattern

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.

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

If you check console, it will be

0 'first'
1 'second'
2 'third'
Enter fullscreen mode Exit fullscreen mode

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.

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]);

  .....
});
Enter fullscreen mode Exit fullscreen mode

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

The descendant pattern can be applied to other components as well. So, we will extract it into separate hooks and providers.

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

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 hook library.

Checkpoint: 9dd86997b99951fa035be73230f8381d3ea189ba

Top comments (0)