diff --git a/packages/teamplay/react/wrapIntoSuspense.js b/packages/teamplay/react/wrapIntoSuspense.js index 3187778..baa69fd 100644 --- a/packages/teamplay/react/wrapIntoSuspense.js +++ b/packages/teamplay/react/wrapIntoSuspense.js @@ -1,31 +1,43 @@ // useSyncExternalStore is used to trigger an update same as in MobX // ref: https://github.com/mobxjs/mobx/blob/94bc4997c14152ff5aefcaac64d982d5c21ba51a/packages/mobx-react-lite/src/useObserver.ts -import { useSyncExternalStore, forwardRef as _forwardRef, memo, createElement as el, Suspense, useId, useRef } from 'react' -import { pipeComponentMeta, pipeComponentDisplayName, ComponentMetaContext } from './helpers.js' +import { + useSyncExternalStore, + forwardRef as _forwardRef, + memo, + createElement as el, + Suspense, + useId, + useRef, +} from "react"; +import { + pipeComponentMeta, + pipeComponentDisplayName, + ComponentMetaContext, +} from "./helpers.js"; // TODO: probably add FinalizationRegistry to handle destruction of observer() before it ever mounted. // In such case we might have a memory leak because subscribe() would never fire and would never // clean up the cache -function destroyAdm (adm) { - adm.onStoreChange = undefined - adm.scheduledUpdatePromise = undefined - adm.scheduleUpdate = undefined - adm.cache?.clear() - adm.cache = undefined +function destroyAdm(adm) { + adm.onStoreChange = undefined; + adm.scheduledUpdatePromise = undefined; + adm.scheduleUpdate = undefined; + adm.cache?.clear(); + adm.cache = undefined; } -export default function wrapIntoSuspense ({ +export default function wrapIntoSuspense({ Component, forwardRef, defer, - suspenseProps = DEFAULT_SUSPENSE_PROPS + suspenseProps = DEFAULT_SUSPENSE_PROPS, } = {}) { - if (!suspenseProps?.fallback) throw Error(ERRORS.noFallback) + if (!suspenseProps?.fallback) throw Error(ERRORS.noFallback); let SuspenseWrapper = (props, ref) => { - const componentId = useId() - const componentMetaRef = useRef() - const admRef = useRef() + const componentId = useId(); + const componentMetaRef = useRef(); + const admRef = useRef(); if (!admRef.current) { const adm = { stateVersion: Symbol(), // eslint-disable-line symbol-description @@ -33,38 +45,38 @@ export default function wrapIntoSuspense ({ scheduledUpdatePromise: undefined, hasPendingUpdate: false, cache: new Map(), - scheduleUpdate: promise => { - if (!promise?.then) throw Error('scheduleUpdate() expects a promise') - if (adm.scheduledUpdatePromise === promise) return - adm.scheduledUpdatePromise = promise + scheduleUpdate: (promise) => { + if (!promise?.then) throw Error("scheduleUpdate() expects a promise"); + if (adm.scheduledUpdatePromise === promise) return; + adm.scheduledUpdatePromise = promise; promise.then(() => { - if (adm.scheduledUpdatePromise !== promise) return - adm.scheduledUpdatePromise = undefined - adm.onStoreChange?.() - }) + if (adm.scheduledUpdatePromise !== promise) return; + adm.scheduledUpdatePromise = undefined; + adm.onStoreChange?.(); + }); }, - subscribe (onStoreChange) { + subscribe(onStoreChange) { adm.onStoreChange = () => { - adm.stateVersion = Symbol() // eslint-disable-line symbol-description - onStoreChange() - } + adm.stateVersion = Symbol(); // eslint-disable-line symbol-description + onStoreChange(); + }; // If there was a pending update before subscribe was called, trigger it asynchronously // to avoid updating during the subscribe/render phase if (adm.hasPendingUpdate) { - adm.hasPendingUpdate = false - queueMicrotask(() => adm.onStoreChange()) + adm.hasPendingUpdate = false; + queueMicrotask(() => adm.onStoreChange?.()); } - return () => destroyAdm(adm) + return () => destroyAdm(adm); + }, + getSnapshot() { + return adm.stateVersion; }, - getSnapshot () { - return adm.stateVersion - } - } - admRef.current = adm + }; + admRef.current = adm; } - const adm = admRef.current + const adm = admRef.current; - useSyncExternalStore(adm.subscribe, adm.getSnapshot, adm.getSnapshot) + useSyncExternalStore(adm.subscribe, adm.getSnapshot, adm.getSnapshot); if (!componentMetaRef.current) { componentMetaRef.current = { @@ -73,47 +85,52 @@ export default function wrapIntoSuspense ({ defer, triggerUpdate: () => { if (adm.onStoreChange) { - adm.onStoreChange() + adm.onStoreChange(); } else { // Save pending update - subscribe not called yet (e.g., from useEffect/useLayoutEffect) - adm.hasPendingUpdate = true + adm.hasPendingUpdate = true; } }, - scheduleUpdate: promise => adm.scheduleUpdate?.(promise), + scheduleUpdate: (promise) => adm.scheduleUpdate?.(promise), cache: { - get: key => adm.cache?.get(key), + get: (key) => adm.cache?.get(key), set: (key, value) => adm.cache?.set(key, value), - has: key => adm.cache?.has(key) - } - } + has: (key) => adm.cache?.has(key), + }, + }; } - if (forwardRef) props = { ...props, ref } + if (forwardRef) props = { ...props, ref }; - return ( - el(ComponentMetaContext.Provider, { value: componentMetaRef.current }, - el(Suspense, suspenseProps, - el(Component, props) - ) - ) - ) - } + return el( + ComponentMetaContext.Provider, + { value: componentMetaRef.current }, + el(Suspense, suspenseProps, el(Component, props)), + ); + }; // pipe only displayName because forwardRef render function // do not support propTypes or defaultProps - pipeComponentDisplayName(Component, SuspenseWrapper, 'StartupjsObserverWrapper') + pipeComponentDisplayName( + Component, + SuspenseWrapper, + "StartupjsObserverWrapper", + ); - if (forwardRef) SuspenseWrapper = _forwardRef(SuspenseWrapper) - SuspenseWrapper = memo(SuspenseWrapper) + if (forwardRef) SuspenseWrapper = _forwardRef(SuspenseWrapper); + SuspenseWrapper = memo(SuspenseWrapper); - pipeComponentMeta(Component, SuspenseWrapper) + pipeComponentMeta(Component, SuspenseWrapper); - return SuspenseWrapper + return SuspenseWrapper; } -const DEFAULT_SUSPENSE_PROPS = { fallback: el(NullComponent, null, null) } -function NullComponent () { return null } +const DEFAULT_SUSPENSE_PROPS = { fallback: el(NullComponent, null, null) }; +function NullComponent() { + return null; +} const ERRORS = { - noFallback: '[observer()] You must pass at least a fallback parameter to suspenseProps' -} + noFallback: + "[observer()] You must pass at least a fallback parameter to suspenseProps", +};