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