diff --git a/.changeset/full-clubs-train.md b/.changeset/full-clubs-train.md new file mode 100644 index 0000000000..145e07d1cb --- /dev/null +++ b/.changeset/full-clubs-train.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/react": patch +--- + +When `runWithForce` is called, we should increment `vnode._original` to make sure the component is re-rendered. This can fix the issue that the component is not re-rendered when updateGlobalProps is called and the root vnode is not a component vnode. diff --git a/packages/react/runtime/__test__/lifecycle/updateGlobalProps.test.jsx b/packages/react/runtime/__test__/lifecycle/updateGlobalProps.test.jsx index e5c5eda9c3..4380dd608e 100644 --- a/packages/react/runtime/__test__/lifecycle/updateGlobalProps.test.jsx +++ b/packages/react/runtime/__test__/lifecycle/updateGlobalProps.test.jsx @@ -2,16 +2,17 @@ // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. -import { beforeEach } from 'vitest'; +import { beforeEach, beforeAll, afterEach, vi } from 'vitest'; import { __root } from '../../src/root'; import { globalEnvManager } from '../utils/envManager'; import { describe } from 'vitest'; import { it } from 'vitest'; import { expect } from 'vitest'; import { render } from 'preact'; -import { waitSchedule } from '../utils/nativeMethod'; -import { beforeAll } from 'vitest'; +import { waitSchedule, elementTree } from '../utils/nativeMethod'; import { replaceCommitHook } from '../../src/lifecycle/patch/commit'; +import { wrapWithLynxComponent, ComponentFromReactRuntime } from '../../src/compat/lynxComponent'; +import { deinitGlobalSnapshotPatch } from '../../src/lifecycle/patch/snapshotPatch'; beforeAll(() => { replaceCommitHook(); @@ -21,6 +22,12 @@ beforeEach(() => { globalEnvManager.resetEnv(); }); +afterEach(() => { + deinitGlobalSnapshotPatch(); + elementTree.clear(); + vi.restoreAllMocks(); +}); + describe('updateGlobalProps', () => { it('should update global props', async () => { lynx.__globalProps = { theme: 'dark' }; @@ -103,4 +110,205 @@ describe('updateGlobalProps', () => { `); } }); + + it('should update global props with root page element', async () => { + lynx.__globalProps = { theme: 'dark' }; + + class C extends ComponentFromReactRuntime { + render() { + return {lynx.__globalProps.theme}; + } + } + + const jsx = ( + + + {lynx.__globalProps.theme} + + ); + // main thread render + { + __root.__jsx = jsx; + renderPage(); + expect(__root.__element_root).toMatchInlineSnapshot(` + + + + + + + + + `); + } + + // background render + { + globalEnvManager.switchToBackground(); + __root.__jsx = jsx; + render(jsx, __root); + } + + // hydrate + { + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + } + + // rLynxChange + { + globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + expect(globalThis.__OnLifecycleEvent).not.toBeCalled(); + await waitSchedule(); + expect(__root.__element_root).toMatchInlineSnapshot(` + + + + + + + + + `); + } + + // updateGlobalProps with addComponentElement enabled + { + globalEnvManager.switchToBackground(); + lynx.getNativeApp().callLepusMethod.mockClear(); + lynxCoreInject.tt.updateGlobalProps({ theme: 'light' }); + await waitSchedule(); + + globalEnvManager.switchToMainThread(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + + // cannot update elements in root render + expect(__root.__element_root).toMatchInlineSnapshot(` + + + + + + + + + `); + } + }); + + it('should update global props with addComponentElement enabled', async () => { + lynx.__globalProps = { theme: 'dark' }; + + class C extends ComponentFromReactRuntime { + render() { + return {lynx.__globalProps.theme}; + } + } + + const jsx = wrapWithLynxComponent((__c) => {__c}, ); + + // main thread render + { + __root.__jsx = jsx; + renderPage(); + expect(__root.__element_root).toMatchInlineSnapshot(` + + + + + + + + `); + } + + // background render + { + globalEnvManager.switchToBackground(); + __root.__jsx = jsx; + render(jsx, __root); + } + + // hydrate + { + lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]); + } + + // rLynxChange + { + globalEnvManager.switchToMainThread(); + globalThis.__OnLifecycleEvent.mockClear(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + expect(globalThis.__OnLifecycleEvent).not.toBeCalled(); + await waitSchedule(); + expect(__root.__element_root).toMatchInlineSnapshot(` + + + + + + + + `); + } + + // updateGlobalProps with addComponentElement enabled + { + globalEnvManager.switchToBackground(); + lynx.getNativeApp().callLepusMethod.mockClear(); + lynxCoreInject.tt.updateGlobalProps({ theme: 'light' }); + await waitSchedule(); + + globalEnvManager.switchToMainThread(); + const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0]; + globalThis[rLynxChange[0]](rLynxChange[1]); + + expect(__root.__element_root).toMatchInlineSnapshot(` + + + + + + + + `); + } + }); }); diff --git a/packages/react/runtime/src/lynx/runWithForce.ts b/packages/react/runtime/src/lynx/runWithForce.ts index efa79dc970..272acedc4b 100644 --- a/packages/react/runtime/src/lynx/runWithForce.ts +++ b/packages/react/runtime/src/lynx/runWithForce.ts @@ -31,6 +31,9 @@ export function runWithForce(cb: () => void): void { const c = oldVNode[COMPONENT]; if (c) { + if (vnode[ORIGINAL] != null && oldVNode[ORIGINAL] === vnode[ORIGINAL]) { + vnode[ORIGINAL] += 1; + } c[FORCE] = true; } else { // mount phase of a new Component