Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/full-clubs-train.md
Original file line number Diff line number Diff line change
@@ -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.
214 changes: 211 additions & 3 deletions packages/react/runtime/__test__/lifecycle/updateGlobalProps.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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' };
Expand Down Expand Up @@ -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 <text>{lynx.__globalProps.theme}</text>;
}
}

const jsx = (
<page class={lynx.__globalProps.theme}>
<C />
<text>{lynx.__globalProps.theme}</text>
</page>
);
// main thread render
{
__root.__jsx = jsx;
renderPage();
expect(__root.__element_root).toMatchInlineSnapshot(`
<page
class="dark"
cssId="default-entry-from-native:0"
>
<text>
<raw-text
text="dark"
/>
</text>
<text>
<raw-text
text="dark"
/>
</text>
</page>
`);
}

// 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(`
<page
class="dark"
cssId="default-entry-from-native:0"
>
<text>
<raw-text
text="dark"
/>
</text>
<text>
<raw-text
text="dark"
/>
</text>
</page>
`);
}

// 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(`
<page
class="dark"
cssId="default-entry-from-native:0"
>
<text>
<raw-text
text="light"
/>
</text>
<text>
<raw-text
text="dark"
/>
</text>
</page>
`);
}
});

it('should update global props with addComponentElement enabled', async () => {
lynx.__globalProps = { theme: 'dark' };

class C extends ComponentFromReactRuntime {
render() {
return <text>{lynx.__globalProps.theme}</text>;
}
}

const jsx = wrapWithLynxComponent((__c) => <view>{__c}</view>, <C />);

// main thread render
{
__root.__jsx = jsx;
renderPage();
expect(__root.__element_root).toMatchInlineSnapshot(`
<page
cssId="default-entry-from-native:0"
>
<view>
<text>
<raw-text
text="dark"
/>
</text>
</view>
</page>
`);
}

// 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(`
<page
cssId="default-entry-from-native:0"
>
<view>
<text>
<raw-text
text="dark"
/>
</text>
</view>
</page>
`);
}

// 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(`
<page
cssId="default-entry-from-native:0"
>
<view>
<text>
<raw-text
text="light"
/>
</text>
</view>
</page>
`);
}
});
});
3 changes: 3 additions & 0 deletions packages/react/runtime/src/lynx/runWithForce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export function runWithForce(cb: () => void): void {

const c = oldVNode[COMPONENT];
if (c) {
if (vnode[ORIGINAL] != null && oldVNode[ORIGINAL] === vnode[ORIGINAL]) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (vnode[ORIGINAL] != null && oldVNode[ORIGINAL] === vnode[ORIGINAL]) {
if (vnode !=== oldVNode && vnode[ORIGINAL] != null && oldVNode[ORIGINAL] === vnode[ORIGINAL]) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove line 11-22 if we do this.

// In https://github.com/preactjs/preact/pull/4724, preact will
// skip render if the `vnode._original` is not changed, even if `c._force` is true
// So we need to increment `vnode._original` to make sure the `__root.__jsx` is re-rendered
// This is the same logic with: https://github.com/preactjs/preact/blob/43178581442fa0f2428e5bdbca355860b2d12e5d/src/component.js#L131
if (__root.__jsx) {
const newVNode = Object.assign({}, __root.__jsx) as unknown as VNode;
if (newVNode[ORIGINAL] != null) {
newVNode[ORIGINAL] += 1;
// @ts-expect-error: __root.__jsx is a VNode
__root.__jsx = newVNode;
}
}

vnode[ORIGINAL] += 1;
}
c[FORCE] = true;
} else {
// mount phase of a new Component
Expand Down
Loading