diff --git a/src/components/Accordion.tsx b/src/components/Accordion.tsx index 6841ff3..392fb97 100644 --- a/src/components/Accordion.tsx +++ b/src/components/Accordion.tsx @@ -6,12 +6,13 @@ import { ReactNode, useCallback, useContext, - useId, - useLayoutEffect, - useRef, - useState, } from "react"; import { useControlledState } from "../utils/useControlledState.ts"; +import { + Descendants, + useDescendant, + useDescendants, +} from "../utils/descendants.tsx"; const ACCORDION_NAME = "Accordion"; const ITEM_NAME = "AccordionItem"; @@ -93,38 +94,22 @@ const Accordion = forwardRef(function ( [multiple, collapsible, onChange, controlledIndex] ); - const indexCounter = useRef(-1); - const descendantsMap = useRef>({}); - - // This useLayoutEffect cleanup is to cleanup the extra render in the strict mode - // Cleanup hack for now - useLayoutEffect(() => { - return () => { - indexCounter.current = -1; - descendantsMap.current = {}; - }; - }, []); - - const getIndex = useCallback((id: string) => { - if (!descendantsMap.current[id]) { - descendantsMap.current[id] = ++indexCounter.current; - } - return descendantsMap.current[id]; - }, []); - const context = { openPanels: controlledIndex ? controlledIndex : openPanels, onAccordionItemClick: readOnly ? noop : onAccordionItemClick, readOnly, - getIndex, }; + const descendantContext = useDescendants(); + return ( - - - {children} - - + + + + {children} + + + ); }); @@ -137,14 +122,8 @@ const AccordionItem = forwardRef(function ( }: AccordionItemProps, forwardedRef ) { - const { openPanels, getIndex } = useAccordionContext(); - - const itemId = useRef(useId()); - const [index, setIndex] = useState(-1); - - useLayoutEffect(() => { - setIndex(getIndex(itemId.current)); - }, [getIndex]); + const { openPanels } = useAccordionContext(); + const index = useDescendant(); const state = (Array.isArray(openPanels) @@ -287,7 +266,6 @@ interface InternalAccordionContextValue { onAccordionItemClick(index: AccordionIndex): void; readOnly: boolean; - getIndex: (id: string) => number; } interface InternalAccordionItemContextValue { diff --git a/src/utils/descendants.tsx b/src/utils/descendants.tsx new file mode 100644 index 0000000..6c22a4f --- /dev/null +++ b/src/utils/descendants.tsx @@ -0,0 +1,82 @@ +import { + createContext, + ReactNode, + useCallback, + useContext, + useRef, + useState, +} from "react"; +import { useIsomorphicLayoutEffect } from "./useIsomorphicEffect.ts"; + +const randomId = () => Math.random().toString(36).substring(2, 9); + +const DescendantContext = createContext( + {} as DescendantProviderProps +); + +export const Descendants = ({ + children, + value, +}: { + children: ReactNode; + value: DescendantProviderProps; +}) => { + // On every re-render of children, reset the count + value.reset(); + + return ( + + {children} + + ); +}; + +export const useDescendants = () => { + const indexCounter = useRef(0); + const map = useRef>({}); + + 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 }; +}; + +/** + * Return index of the current item within its parent's list + * @param {any} props - Props that will be exposed to the parent list + */ +export const useDescendant = (props?: Record) => { + const context = useContext(DescendantContext); + const descendantId = useRef(); + if (!descendantId.current) { + descendantId.current = randomId(); + } + + const [index, setIndex] = useState(-1); + + useIsomorphicLayoutEffect(() => { + setIndex(context?.getIndex(descendantId.current as string, props)); + }, []); + + return index; +}; + +type DescendantProviderProps = ReturnType; + +interface IgetIndexProps { + hidden?: boolean; + + [key: string]: any; +} diff --git a/src/utils/useIsomorphicEffect.ts b/src/utils/useIsomorphicEffect.ts new file mode 100644 index 0000000..852f1cf --- /dev/null +++ b/src/utils/useIsomorphicEffect.ts @@ -0,0 +1,8 @@ +import { useEffect, useLayoutEffect } from "react"; + +/* + useLayoutEffect is a browser hook, so for React code generated from the server it will give error. + For more details check: https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect + */ +export const useIsomorphicLayoutEffect = + typeof window !== "undefined" ? useLayoutEffect : useEffect;