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>
If you check console, it will be
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.
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 withgetIndex
. At this point,descendantsMap
retrieves the ID of theAccordionItem
, records that ID, and returns the index. - With each call to
getIndex
(with each render ofAccordionItem
),indexCounter
increments by 1. - In this way
descendantMap
ofAccordion
will keep track of eachAccordionItem
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;
};
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)