Skip to content

Commit

Permalink
Feat: Create separete hooks and context to handle descendants
Browse files Browse the repository at this point in the history
  • Loading branch information
HariBhandari07 committed Jan 17, 2024
1 parent 50c3a0b commit 9dd8699
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 38 deletions.
54 changes: 16 additions & 38 deletions src/components/Accordion.tsx
Expand Up @@ -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";
Expand Down Expand Up @@ -93,38 +94,22 @@ const Accordion = forwardRef(function (
[multiple, collapsible, onChange, controlledIndex]
);

const indexCounter = useRef(-1);
const descendantsMap = useRef<Record<string, number>>({});

// 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 (
<AccordionContext.Provider value={context}>
<Comp {...props} ref={forwardedRef} data-hb-accordion="">
{children}
</Comp>
</AccordionContext.Provider>
<Descendants value={descendantContext}>
<AccordionContext.Provider value={context}>
<Comp {...props} ref={forwardedRef} data-hb-accordion="">
{children}
</Comp>
</AccordionContext.Provider>
</Descendants>
);
});

Expand All @@ -137,14 +122,8 @@ const AccordionItem = forwardRef(function (
}: AccordionItemProps,
forwardedRef
) {
const { openPanels, getIndex } = useAccordionContext();

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

useLayoutEffect(() => {
setIndex(getIndex(itemId.current));
}, [getIndex]);
const { openPanels } = useAccordionContext();
const index = useDescendant();

const state =
(Array.isArray(openPanels)
Expand Down Expand Up @@ -287,7 +266,6 @@ interface InternalAccordionContextValue {
onAccordionItemClick(index: AccordionIndex): void;

readOnly: boolean;
getIndex: (id: string) => number;
}

interface InternalAccordionItemContextValue {
Expand Down
82 changes: 82 additions & 0 deletions 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<DescendantProviderProps>(
{} as DescendantProviderProps
);

export const Descendants = ({
children,
value,
}: {
children: ReactNode;
value: DescendantProviderProps;
}) => {
// On every re-render of children, reset the count
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 };
};

/**
* 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<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;
};

type DescendantProviderProps = ReturnType<typeof useDescendants>;

interface IgetIndexProps {
hidden?: boolean;

[key: string]: any;
}
8 changes: 8 additions & 0 deletions 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;

0 comments on commit 9dd8699

Please sign in to comment.