From d375e5c9f162cbd84a6e6462977db56519d09f75 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Thu, 7 Dec 2023 18:48:53 +0100 Subject: refactor(pages): refine Project pages * refactor ProjectOverview component to let consumers handle the value * extract project overview depending on Github to avoid fetching Github API if the project is not on Github * wrap dynamic import in a useMemo hook to avoid infinite rerender * fix table of contents by adding a useMutationObserver hook to refresh headings tree (without it useHeadingsTree is not retriggered once the dynamic import is done) * add Cypress tests --- .../hooks/use-headings-tree/use-headings-tree.ts | 50 +++++++++++++++++----- 1 file changed, 39 insertions(+), 11 deletions(-) (limited to 'src/utils/hooks/use-headings-tree/use-headings-tree.ts') diff --git a/src/utils/hooks/use-headings-tree/use-headings-tree.ts b/src/utils/hooks/use-headings-tree/use-headings-tree.ts index 68bdde8..802d843 100644 --- a/src/utils/hooks/use-headings-tree/use-headings-tree.ts +++ b/src/utils/hooks/use-headings-tree/use-headings-tree.ts @@ -1,5 +1,7 @@ -import { useState, useCallback, type RefCallback } from 'react'; +import { useState, useCallback, type RefCallback, useEffect } from 'react'; import type { HeadingLevel } from '../../../components'; +import type { Nullable } from '../../../types'; +import { useMutationObserver } from '../use-mutation-observer'; export type HeadingsTreeNode = { /** @@ -140,17 +142,43 @@ export const useHeadingsTree = ( 'Invalid options: `fromLevel` must be lower or equal to `toLevel`.' ); - const [tree, setTree] = useState([]); + const [headings, setHeadings] = useState>(); const requestedHeadingTags = getHeadingTagsList(options); const query = requestedHeadingTags.join(', '); - const ref: RefCallback = useCallback( - (el) => { - const headingNodes = el?.querySelectorAll(query); - if (headingNodes) setTree(buildHeadingsTreeFrom(headingNodes)); - }, - [query] - ); - - return { ref, tree }; + /* + * With a mutable ref, the headings are not always updated because of loading + * states. So we need to use a RefCallback to detect when the component is + * effectively rendered. However, to be able to compare the mutation records, + * we need to keep track of the current ref so we also need to use useState... + */ + const [wrapper, setWrapper] = useState>(); + const ref: RefCallback = useCallback((el) => { + setWrapper(el); + }, []); + + const updateHeadings = useCallback(() => { + const headingNodes = wrapper?.querySelectorAll(query); + + if (headingNodes) setHeadings(headingNodes); + }, [query, wrapper]); + + useEffect(() => { + if (wrapper) updateHeadings(); + }, [updateHeadings, wrapper]); + + useMutationObserver({ + callback: useCallback( + (records) => { + for (const record of records) { + if (record.target === wrapper) updateHeadings(); + } + }, + [updateHeadings, wrapper] + ), + options: { childList: true, subtree: true }, + ref: { current: typeof window === 'undefined' ? null : document.body }, + }); + + return { ref, tree: headings ? buildHeadingsTreeFrom(headings) : [] }; }; -- cgit v1.2.3