From 648532d9d3468d78cab81a0613a1825136b646f3 Mon Sep 17 00:00:00 2001 From: caohuilin Date: Tue, 4 Nov 2025 10:33:14 +0800 Subject: [PATCH 1/5] fix: garfish component render for props not update --- packages/runtime/plugin-garfish/package.json | 2 +- .../plugin-garfish/src/runtime/utils/apps.tsx | 108 +++++++++++++++++- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/packages/runtime/plugin-garfish/package.json b/packages/runtime/plugin-garfish/package.json index fad929cb1af4..338b260b72c9 100644 --- a/packages/runtime/plugin-garfish/package.json +++ b/packages/runtime/plugin-garfish/package.json @@ -15,7 +15,7 @@ "modern", "modern.js" ], - "version": "2.68.18", + "version": "2.68.19-alpha.1", "jsnext:source": "./src/cli/index.ts", "types": "./src/cli/index.ts", "typesVersions": { diff --git a/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx b/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx index c7f088b9b2c8..577a42ef5f1b 100644 --- a/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx +++ b/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx @@ -38,6 +38,42 @@ export function pathJoin(...args: string[]) { return res || '/'; } +function deepEqualExcludeFunctions(prev: any, next: any): boolean { + if (prev === next) return true; + if (!prev || !next) return false; + if (typeof prev !== 'object' || typeof next !== 'object') return false; + + const prevKeys = Object.keys(prev).filter( + key => typeof prev[key] !== 'function', + ); + const nextKeys = Object.keys(next).filter( + key => typeof next[key] !== 'function', + ); + + if (prevKeys.length !== nextKeys.length) return false; + + for (const key of prevKeys) { + if (!nextKeys.includes(key)) return false; + + const prevVal = prev[key]; + const nextVal = next[key]; + + if (typeof prevVal === 'function' || typeof nextVal === 'function') { + continue; + } + + if (typeof prevVal === 'object' && typeof nextVal === 'object') { + if (!deepEqualExcludeFunctions(prevVal, nextVal)) { + return false; + } + } else if (prevVal !== nextVal) { + return false; + } + } + + return true; +} + function getAppInstance( options: typeof Garfish.options, appInfo: ModulesInfo[number], @@ -55,6 +91,9 @@ function getAppInstance( function MicroApp(props: MicroProps) { const appRef = useRef(null); const locationHrefRef = useRef(''); + const propsRef = useRef(props); + const previousPropsRef = useRef(props); + const propsUpdateCounterRef = useRef(0); const domId = generateSubAppContainerKey(appInfo); const [{ component: SubModuleComponent }, setSubModuleComponent] = @@ -63,6 +102,7 @@ function getAppInstance( }>({ component: null, }); + const [propsUpdateKey, setPropsUpdateKey] = useState(0); const context = useContext(RuntimeReactContext); const useRouteMatch = props.useRouteMatch ?? context?.router?.useRouteMatch; const useMatches = props.useMatches ?? context?.router?.useMatches; @@ -157,11 +197,47 @@ or directly pass the "basename": } }, [locationPathname]); + useEffect(() => { + const prevPropsForCompare = { ...previousPropsRef.current }; + const currentPropsForCompare = { ...props }; + + Object.keys(prevPropsForCompare).forEach(key => { + if (typeof prevPropsForCompare[key] === 'function') { + delete prevPropsForCompare[key]; + } + }); + Object.keys(currentPropsForCompare).forEach(key => { + if (typeof currentPropsForCompare[key] === 'function') { + delete currentPropsForCompare[key]; + } + }); + + if ( + !deepEqualExcludeFunctions(prevPropsForCompare, currentPropsForCompare) + ) { + previousPropsRef.current = props; + propsRef.current = props; + propsUpdateCounterRef.current += 1; + setPropsUpdateKey(prev => prev + 1); + // If the app is mounted, notify the sub-app via a custom event (optional) + if (appRef.current?.mounted) { + window.dispatchEvent( + new CustomEvent('garfishPropsUpdated', { + detail: { + appName: appInfo.name, + props: props, + }, + }), + ); + } + } + }, [props, appInfo.name]); + useEffect(() => { // [MODIFIED] Register the current instance's state setter when the component mounts componentSetterRegistry.current = setSubModuleComponent; - const { setLoadingState, ...userProps } = props; + const { setLoadingState, ...userProps } = propsRef.current; const loadAppOptions: Omit = { cache: true, @@ -278,12 +354,38 @@ or directly pass the "basename": } } }; - }, []); + }, [basename, domId, appInfo.name]); + + useEffect(() => { + if (appRef.current?.appInfo) { + const { setLoadingState, ...updatedProps } = props; + const updatedPropsWithKey = { + ...appInfo.props, + ...updatedProps, + _garfishPropsUpdateKey: propsUpdateKey, + }; + appRef.current.appInfo.props = updatedPropsWithKey; + } + }, [propsUpdateKey, props]); + + // Remove setLoadingState from props + const { setLoadingState, ...renderProps } = props; + + // Create the final props that include _garfishPropsUpdateKey + const finalRenderProps = { + ...renderProps, + _garfishPropsUpdateKey: propsUpdateKey, + }; + + // Use propsUpdateKey as part of the key + const componentKey = `${appInfo.name}-${propsUpdateKey}`; return ( <>
- {SubModuleComponent && } + {SubModuleComponent && ( + + )}
); From f0c8abaf3f543e151cc99416a96e0b3172f7032e Mon Sep 17 00:00:00 2001 From: caohuilin Date: Tue, 4 Nov 2025 10:35:19 +0800 Subject: [PATCH 2/5] docs: changeset --- .changeset/giant-islands-fold.md | 7 +++++++ packages/runtime/plugin-garfish/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/giant-islands-fold.md diff --git a/.changeset/giant-islands-fold.md b/.changeset/giant-islands-fold.md new file mode 100644 index 000000000000..e75500430325 --- /dev/null +++ b/.changeset/giant-islands-fold.md @@ -0,0 +1,7 @@ +--- +'@modern-js/plugin-garfish': patch +--- + +fix: garfish plugin component render not update props + +fix: 修复 garfish 插件组件渲染时没更新子应用 props diff --git a/packages/runtime/plugin-garfish/package.json b/packages/runtime/plugin-garfish/package.json index 338b260b72c9..fad929cb1af4 100644 --- a/packages/runtime/plugin-garfish/package.json +++ b/packages/runtime/plugin-garfish/package.json @@ -15,7 +15,7 @@ "modern", "modern.js" ], - "version": "2.68.19-alpha.1", + "version": "2.68.18", "jsnext:source": "./src/cli/index.ts", "types": "./src/cli/index.ts", "typesVersions": { From 26c877673fd0b51614708d82da399733d779c6c3 Mon Sep 17 00:00:00 2001 From: caohuilin Date: Tue, 4 Nov 2025 10:37:36 +0800 Subject: [PATCH 3/5] feat: remove dispatch event --- .../runtime/plugin-garfish/src/runtime/utils/apps.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx b/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx index 577a42ef5f1b..84680c629629 100644 --- a/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx +++ b/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx @@ -219,17 +219,6 @@ or directly pass the "basename": propsRef.current = props; propsUpdateCounterRef.current += 1; setPropsUpdateKey(prev => prev + 1); - // If the app is mounted, notify the sub-app via a custom event (optional) - if (appRef.current?.mounted) { - window.dispatchEvent( - new CustomEvent('garfishPropsUpdated', { - detail: { - appName: appInfo.name, - props: props, - }, - }), - ); - } } }, [props, appInfo.name]); From 90a43e2d11986e8c19a08efd329efe1d4e58e13e Mon Sep 17 00:00:00 2001 From: caohuilin Date: Mon, 10 Nov 2025 19:54:21 +0800 Subject: [PATCH 4/5] fix: app render error --- .../plugin-garfish/src/runtime/utils/apps.tsx | 84 ++++++++++++++++--- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx b/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx index 84680c629629..b7226d11b77d 100644 --- a/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx +++ b/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx @@ -84,7 +84,10 @@ function getAppInstance( // It will be shared by all MicroApp component instances to store the state setter of the currently active component const componentSetterRegistry = { current: null as React.Dispatch< - React.SetStateAction<{ component: React.ComponentType | null }> + React.SetStateAction<{ + component: React.ComponentType | null; + isFromJupiter?: boolean; + }> > | null, }; @@ -96,12 +99,17 @@ function getAppInstance( const propsUpdateCounterRef = useRef(0); const domId = generateSubAppContainerKey(appInfo); - const [{ component: SubModuleComponent }, setSubModuleComponent] = - useState<{ - component: React.ComponentType | null; - }>({ - component: null, - }); + const componentRef = useRef | null>(null); + const [ + { component: SubModuleComponent, isFromJupiter }, + setSubModuleComponent, + ] = useState<{ + component: React.ComponentType | null; + isFromJupiter?: boolean; + }>({ + component: null, + isFromJupiter: false, + }); const [propsUpdateKey, setPropsUpdateKey] = useState(0); const context = useContext(RuntimeReactContext); const useRouteMatch = props.useRouteMatch ?? context?.router?.useRouteMatch; @@ -249,6 +257,8 @@ or directly pass the "basename": jupiter_submodule_app_key, } = provider; const SubComponent = SubModuleComponent || jupiter_submodule_app_key; + const isFromJupiter = + !SubModuleComponent && !!jupiter_submodule_app_key; const componetRenderMode = manifest?.componentRender; return { mount: (...props) => { @@ -256,7 +266,11 @@ or directly pass the "basename": // [MODIFIED] Get and call the current state setter from the registry center // This way, even if the mount method is cached, it can still call the setter of the latest component instance if (componentSetterRegistry.current) { - componentSetterRegistry.current({ component: SubComponent }); + componentRef.current = SubComponent; + componentSetterRegistry.current({ + component: SubComponent, + isFromJupiter, + }); } else { logger( `[Garfish] MicroApp for "${appInfo.name}" tried to mount, but no active component setter was found.`, @@ -357,6 +371,50 @@ or directly pass the "basename": } }, [propsUpdateKey, props]); + useEffect(() => { + const componetRenderMode = manifest?.componentRender; + + // 只在 componentRender 模式下,且应用已挂载时执行 + if (componetRenderMode && appRef.current?.mounted) { + // 使用 SubModuleComponent 或 componentRef.current 来获取最新的组件引用 + const componentToUse = SubModuleComponent || componentRef.current; + + // 如果组件存在,则强制重新挂载 + if (componentToUse) { + // 当 propsUpdateKey 变化时,清除组件状态,强制 React 重新挂载组件 + // 通过设置 component 为 null,然后延迟恢复,确保 React 能够检测到组件状态的变化 + const currentComponent = componentToUse; + const currentIsFromJupiter = isFromJupiter; + + // 清除组件,触发 React 卸载 + setSubModuleComponent({ + component: null, + isFromJupiter: false, + }); + + // 使用 setTimeout 延迟恢复,确保 React 能够完全卸载组件后再重新挂载 + // 这样可以确保组件真正重新挂载,而不是仅仅更新 props + setTimeout(() => { + setSubModuleComponent({ + component: currentComponent, + isFromJupiter: currentIsFromJupiter, + }); + }, 50); + } else { + // 组件还未设置,但应用已挂载,可以尝试重新触发 mount + // 即使组件为 null,只要应用已挂载,我们也可以尝试重新触发 mount 来设置组件 + if (appRef.current?.mounted) { + // 先 hide,然后 show,触发组件重新设置 + appRef.current?.hide(); + setTimeout(() => { + appRef.current?.show(); + }, 10); + } + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [propsUpdateKey]); + // Remove setLoadingState from props const { setLoadingState, ...renderProps } = props; @@ -367,13 +425,19 @@ or directly pass the "basename": }; // Use propsUpdateKey as part of the key - const componentKey = `${appInfo.name}-${propsUpdateKey}`; + // If the component is from jupiter_submodule_app_key, don't use the update key calculation logic + const componentKey = isFromJupiter + ? undefined + : `${appInfo.name}-${propsUpdateKey}`; return ( <>
{SubModuleComponent && ( - + )}
From 52dec1734f13f5972dab5d166bbaba75ea6593e0 Mon Sep 17 00:00:00 2001 From: caohuilin Date: Tue, 11 Nov 2025 13:49:12 +0800 Subject: [PATCH 5/5] feat: add ignore keys for update --- .../plugin-garfish/src/runtime/utils/apps.tsx | 68 +++++++++++++++++-- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx b/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx index b7226d11b77d..572ce42dcf7e 100644 --- a/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx +++ b/packages/runtime/plugin-garfish/src/runtime/utils/apps.tsx @@ -38,11 +38,22 @@ export function pathJoin(...args: string[]) { return res || '/'; } -function deepEqualExcludeFunctions(prev: any, next: any): boolean { +function deepEqualExcludeFunctions( + prev: any, + next: any, + visited?: WeakSet, +): boolean { if (prev === next) return true; if (!prev || !next) return false; if (typeof prev !== 'object' || typeof next !== 'object') return false; + const visitedSet = visited ?? new WeakSet(); + // 如果已经访问过,说明有循环引用,直接返回 true(认为相等) + if (visitedSet.has(prev) || visitedSet.has(next)) { + return true; + } + visitedSet.add(prev); + visitedSet.add(next); const prevKeys = Object.keys(prev).filter( key => typeof prev[key] !== 'function', ); @@ -63,7 +74,7 @@ function deepEqualExcludeFunctions(prev: any, next: any): boolean { } if (typeof prevVal === 'object' && typeof nextVal === 'object') { - if (!deepEqualExcludeFunctions(prevVal, nextVal)) { + if (!deepEqualExcludeFunctions(prevVal, nextVal, visitedSet)) { return false; } } else if (prevVal !== nextVal) { @@ -117,6 +128,8 @@ function getAppInstance( const useLocation = props.useLocation ?? context?.router?.useLocation; const useHistory = props.useHistory ?? context?.router?.useHistory; const useHref = props.useHistory ?? context?.router?.useHref; + const lastPropsUpdateKeyRef = useRef(0); + const isRemountingRef = useRef(false); const match = useRouteMatch?.(); const matchs = useMatches?.(); @@ -206,9 +219,22 @@ or directly pass the "basename": }, [locationPathname]); useEffect(() => { + if (previousPropsRef.current === props) { + return; + } const prevPropsForCompare = { ...previousPropsRef.current }; const currentPropsForCompare = { ...props }; + const ignoredKeysForRemount = [ + 'style', + 'location', + 'match', + 'history', + 'staticContext', + 'guideState', + 'guideConfig', + ]; + Object.keys(prevPropsForCompare).forEach(key => { if (typeof prevPropsForCompare[key] === 'function') { delete prevPropsForCompare[key]; @@ -220,13 +246,34 @@ or directly pass the "basename": } }); - if ( - !deepEqualExcludeFunctions(prevPropsForCompare, currentPropsForCompare) - ) { + const prevPropsForDeepCompare: any = {}; + const currentPropsForDeepCompare: any = {}; + + Object.keys(prevPropsForCompare).forEach(key => { + if (!ignoredKeysForRemount.includes(key)) { + prevPropsForDeepCompare[key] = prevPropsForCompare[key]; + } + }); + Object.keys(currentPropsForCompare).forEach(key => { + if (!ignoredKeysForRemount.includes(key)) { + currentPropsForDeepCompare[key] = currentPropsForCompare[key]; + } + }); + + // 只对非路由相关的 props 进行深度比较 + const propsEqual = deepEqualExcludeFunctions( + prevPropsForDeepCompare, + currentPropsForDeepCompare, + ); + + if (!propsEqual) { previousPropsRef.current = props; propsRef.current = props; propsUpdateCounterRef.current += 1; setPropsUpdateKey(prev => prev + 1); + } else { + previousPropsRef.current = props; + propsRef.current = props; } }, [props, appInfo.name]); @@ -374,6 +421,14 @@ or directly pass the "basename": useEffect(() => { const componetRenderMode = manifest?.componentRender; + if ( + propsUpdateKey === lastPropsUpdateKeyRef.current || + isRemountingRef.current + ) { + return; + } + lastPropsUpdateKeyRef.current = propsUpdateKey; + // 只在 componentRender 模式下,且应用已挂载时执行 if (componetRenderMode && appRef.current?.mounted) { // 使用 SubModuleComponent 或 componentRef.current 来获取最新的组件引用 @@ -408,6 +463,9 @@ or directly pass the "basename": appRef.current?.hide(); setTimeout(() => { appRef.current?.show(); + setTimeout(() => { + isRemountingRef.current = false; + }, 100); }, 10); } }