diff --git a/packages/core/admin/admin/src/content-manager/components/NonRepeatableComponent/index.js b/packages/core/admin/admin/src/content-manager/components/NonRepeatableComponent/index.js index 721472d1ab..06c0031153 100644 --- a/packages/core/admin/admin/src/content-manager/components/NonRepeatableComponent/index.js +++ b/packages/core/admin/admin/src/content-manager/components/NonRepeatableComponent/index.js @@ -9,6 +9,7 @@ import { Stack } from '@strapi/design-system/Stack'; import { useContentTypeLayout } from '../../hooks'; import FieldComponent from '../FieldComponent'; import Inputs from '../Inputs'; +import useLazyComponents from '../../hooks/useLazyComponents'; const NonRepeatableComponent = ({ componentUid, isFromDynamicZone, isNested, name }) => { const { getComponentLayout } = useContentTypeLayout(); @@ -18,6 +19,8 @@ const NonRepeatableComponent = ({ componentUid, isFromDynamicZone, isNested, nam ); const fields = componentLayoutData.layouts.edit; + const { lazyComponentStore } = useLazyComponents(); + return ( ); diff --git a/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/components/Component.js b/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/components/Component.js index 48401aa348..ac91892f46 100644 --- a/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/components/Component.js +++ b/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/components/Component.js @@ -28,6 +28,7 @@ import Inputs from '../../Inputs'; import FieldComponent from '../../FieldComponent'; import Preview from './Preview'; +import useLazyComponents from '../../../hooks/useLazyComponents'; const CustomIconButton = styled(IconButton)` background-color: transparent; @@ -124,6 +125,8 @@ const DraggedItem = ({ const composedAccordionRefs = composeRefs(accordionRef, dragRef); const composedBoxRefs = composeRefs(boxRef, dropRef); + const { lazyComponentStore } = useLazyComponents(); + return ( {isDragging ? ( @@ -212,6 +215,7 @@ const DraggedItem = ({ metadatas={metadatas} queryInfos={queryInfos} size={size} + customFieldInputs={lazyComponentStore} /> ); diff --git a/packages/core/admin/admin/src/content-manager/hooks/useLazyComponents/index.js b/packages/core/admin/admin/src/content-manager/hooks/useLazyComponents/index.js index 1b3ddf0462..59bd9f4904 100644 --- a/packages/core/admin/admin/src/content-manager/hooks/useLazyComponents/index.js +++ b/packages/core/admin/admin/src/content-manager/hooks/useLazyComponents/index.js @@ -1,44 +1,69 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useCustomFields } from '@strapi/helper-plugin'; +const componentStore = new Map(); + /** * @description * A hook to lazy load custom field components * @param {Array.} componentUids - The uids to look up components * @returns object */ -const useLazyComponents = (componentUids) => { - const [lazyComponentStore, setLazyComponentStore] = useState({}); - const [loading, setLoading] = useState(true); +const useLazyComponents = (componentUids = []) => { + const [lazyComponentStore, setLazyComponentStore] = useState(Object.fromEntries(componentStore)); + const [loading, setLoading] = useState(() => { + if (componentStore.size === 0 && componentUids.length > 0) { + return true; + } + + return false; + }); const customFieldsRegistry = useCustomFields(); useEffect(() => { + const setStore = (store) => { + setLazyComponentStore(store); + setLoading(false); + }; + const lazyLoadComponents = async (uids, components) => { const modules = await Promise.all(components); uids.forEach((uid, index) => { - if (!Object.keys(lazyComponentStore).includes(uid)) { - setLazyComponentStore({ ...lazyComponentStore, [uid]: modules[index].default }); - } + componentStore.set(uid, modules[index].default); }); + + setStore(Object.fromEntries(componentStore)); }; - if (componentUids.length) { - const componentPromises = componentUids.map((uid) => { + if (componentUids.length && loading) { + /** + * These uids are not in the component store therefore we need to get the components + */ + const newUids = componentUids.filter((uid) => !componentStore.get(uid)); + + const componentPromises = newUids.map((uid) => { const customField = customFieldsRegistry.get(uid); return customField.components.Input(); }); - lazyLoadComponents(componentUids, componentPromises); + if (componentPromises.length > 0) { + lazyLoadComponents(newUids, componentPromises); + } } + }, [componentUids, customFieldsRegistry, loading]); - if (componentUids.length === Object.keys(lazyComponentStore).length) { - setLoading(false); - } - }, [componentUids, customFieldsRegistry, loading, lazyComponentStore]); + /** + * Wrap this in a callback so it can be used in + * effects to cleanup the cached store if required + */ + const cleanup = useCallback(() => { + componentStore.clear(); + setLazyComponentStore({}); + }, []); - return { isLazyLoading: loading, lazyComponentStore }; + return { isLazyLoading: loading, lazyComponentStore, cleanup }; }; export default useLazyComponents; diff --git a/packages/core/admin/admin/src/content-manager/hooks/useLazyComponents/tests/useLazyComponents.test.js b/packages/core/admin/admin/src/content-manager/hooks/useLazyComponents/tests/useLazyComponents.test.js index e5effa9a15..db825503be 100644 --- a/packages/core/admin/admin/src/content-manager/hooks/useLazyComponents/tests/useLazyComponents.test.js +++ b/packages/core/admin/admin/src/content-manager/hooks/useLazyComponents/tests/useLazyComponents.test.js @@ -26,25 +26,50 @@ jest.mock('@strapi/helper-plugin', () => ({ })); describe('useLazyComponents', () => { + let cleanup; + + afterEach(() => { + if (typeof cleanup === 'function') { + cleanup(); + cleanup = undefined; + } + }); + it('lazy loads the components', async () => { - const { result, waitForNextUpdate } = renderHook(() => + const { result, waitFor } = renderHook(() => useLazyComponents(['plugin::test.test'])); + + cleanup = result.current.cleanup; + + expect(result.current.isLazyLoading).toEqual(true); + expect(result.current.lazyComponentStore).toEqual({}); + + await waitFor(() => expect(result.current.isLazyLoading).toEqual(false)); + + expect(result.current.lazyComponentStore['plugin::test.test']).toBeDefined(); + }); + + test('assuming the store has been initialised before hand, other hooks called should be able to access the global cache', async () => { + const { result: initialResult, waitFor } = renderHook(() => useLazyComponents(['plugin::test.test']) ); - expect(result.current).toEqual({ isLazyLoading: true, lazyComponentStore: {} }); - - await waitForNextUpdate(); - - expect(JSON.stringify(result.current)).toEqual( - JSON.stringify({ - isLazyLoading: false, - lazyComponentStore: { 'plugin::test.test': jest.fn() }, - }) + await waitFor(() => + expect(initialResult.current.lazyComponentStore['plugin::test.test']).toBeDefined() ); + + const { result: actualResult, waitFor: secondWaitFor } = renderHook(() => useLazyComponents()); + + cleanup = actualResult.current.cleanup; + + await secondWaitFor(() => expect(actualResult.current.isLazyLoading).toBe(false)); + + expect(actualResult.current.lazyComponentStore['plugin::test.test']).toBeDefined(); }); - it('handles no components to load', async () => { + + test('given there are no components to load it should not be loading and the store should be empty', async () => { const { result } = renderHook(() => useLazyComponents([])); - expect(result.current).toEqual({ isLazyLoading: false, lazyComponentStore: {} }); + expect(result.current.isLazyLoading).toEqual(false); + expect(result.current.lazyComponentStore).toEqual({}); }); });