Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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/dry-results-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/react": patch
---

Bump Preact to 10.28.x.
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@
"api-extractor": "api-extractor run --verbose"
},
"dependencies": {
"preact": "npm:@hongzhiyuan/preact@10.24.0-00213bad"
"preact": "npm:@hongzhiyuan/preact@10.28.0-fc4af453"
},
"devDependencies": {
"@lynx-js/types": "3.6.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { expect, test, vi } from 'vitest';
test('preact/debug - Invalid argument passed to hook', async () => {
vi.stubGlobal('__MAIN_THREAD__', false)
.stubGlobal('__LEPUS__', false);
let warnLog = [];
vi.spyOn(console, 'warn').mockImplementation((...args) => {
warnLog.push(args);
});

await import('preact/debug');
const { root, useEffect, useState } = await import('../../src/index');
Expand All @@ -27,7 +31,12 @@ test('preact/debug - Invalid argument passed to hook', async () => {
);
}

expect(() => root.render(<App />)).toThrowErrorMatchingInlineSnapshot(
`[Error: Invalid argument passed to hook. Hooks should not be called with NaN in the dependency array. Hook index 1 in component Bar was called with NaN.]`,
);
root.render(<App />);
expect(warnLog).toMatchInlineSnapshot(`
[
[
"Invalid argument passed to hook. Hooks should not be called with NaN in the dependency array. Hook index 1 in component Bar was called with NaN.",
],
]
`);
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test('preact/debug - Objects are not valid as a child', async () => {

expect(() => root.render(<App />)).toThrowErrorMatchingInlineSnapshot(
`
[Error: Objects are not valid as a child. Encountered an object with the keys {foo,bar,baz,__,__b,__i,__u,__d}.
[Error: Objects are not valid as a child. Encountered an object with the keys {foo,bar,baz,__,__b,__i,__u}.

in Bar
in App
Expand Down
43 changes: 37 additions & 6 deletions packages/react/runtime/__test__/lifecycle/updateData.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,8 @@ describe('triggerDataUpdated', () => {
lynxCoreInject.tt.updateCardData({ msg: 'update' });
await waitSchedule();

expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(3);
// duplicated because of https://github.com/preactjs/preact/pull/4724
expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(3 * 2);
expect(lynx.getNativeApp().callLepusMethod.mock.calls).toMatchInlineSnapshot(
`
[
Expand All @@ -491,7 +492,7 @@ describe('triggerDataUpdated', () => {
[
"rLynxChange",
{
"data": "{"patchList":[{"id":12,"snapshotPatch":[3,-6,0,"update"]}]}",
"data": "{"patchList":[{"id":12}]}",
Comment thread
upupming marked this conversation as resolved.
"patchOptions": {
"flowIds": [
666,
Expand All @@ -504,7 +505,7 @@ describe('triggerDataUpdated', () => {
[
"rLynxChange",
{
"data": "{"patchList":[{"id":13,"snapshotPatch":[3,-7,0,"update"]}]}",
"data": "{"patchList":[{"id":13}]}",
"patchOptions": {
"flowIds": [
666,
Expand All @@ -514,6 +515,36 @@ describe('triggerDataUpdated', () => {
},
[Function],
],
[
"rLynxChange",
{
"data": "{"patchList":[{"id":14}]}",
"patchOptions": {
"reloadVersion": 0,
},
},
[Function],
],
[
"rLynxChange",
{
"data": "{"patchList":[{"id":15,"snapshotPatch":[3,-6,0,"update"]}]}",
"patchOptions": {
"reloadVersion": 0,
},
},
[Function],
],
[
"rLynxChange",
{
"data": "{"patchList":[{"id":16,"snapshotPatch":[3,-7,0,"update"]}]}",
"patchOptions": {
"reloadVersion": 0,
},
},
[Function],
],
]
`,
);
Expand Down Expand Up @@ -648,7 +679,7 @@ describe('triggerDataUpdated', () => {
[
"rLynxChange",
{
"data": "{"patchList":[{"id":16,"snapshotPatch":[3,-3,0,"update"]}],"flushOptions":{"triggerDataUpdated":true}}",
"data": "{"patchList":[{"id":19,"snapshotPatch":[3,-3,0,"update"]}],"flushOptions":{"triggerDataUpdated":true}}",
"patchOptions": {
"flowIds": [
666,
Expand Down Expand Up @@ -958,7 +989,7 @@ describe('flush pending `renderComponent` before hydrate', () => {
globalThis[rLynxChange[0]](rLynxChange[1]);
expect(rLynxChange[1]).toMatchInlineSnapshot(`
{
"data": "{"patchList":[{"snapshotPatch":[],"id":24}]}",
"data": "{"patchList":[{"snapshotPatch":[],"id":27}]}",
"patchOptions": {
"isHydration": true,
"pipelineOptions": {
Expand Down Expand Up @@ -1059,7 +1090,7 @@ describe('flush pending `renderComponent` before hydrate', () => {
globalThis[rLynxChange[0]](rLynxChange[1]);
expect(rLynxChange[1]).toMatchInlineSnapshot(`
{
"data": "{"patchList":[{"snapshotPatch":[3,-3,0,"true"],"id":26}]}",
"data": "{"patchList":[{"snapshotPatch":[3,-3,0,"true"],"id":29}]}",
"patchOptions": {
"isHydration": true,
"pipelineOptions": {
Expand Down
106 changes: 106 additions & 0 deletions packages/react/runtime/__test__/lifecycle/updateGlobalProps.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2026 The Lynx Authors. All rights reserved.
// 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 { __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 { replaceCommitHook } from '../../src/lifecycle/patch/commit';

beforeAll(() => {
replaceCommitHook();
});
Comment thread
upupming marked this conversation as resolved.

beforeEach(() => {
globalEnvManager.resetEnv();
});

describe('updateGlobalProps', () => {
it('should update global props', async () => {
lynx.__globalProps = { theme: 'dark' };
const Comp = () => {
return <text>{lynx.__globalProps.theme}</text>;
};

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

// background render
{
globalEnvManager.switchToBackground();
__root.__jsx = <Comp />;
render(<Comp />, __root);
}

// hydrate
{
// LifecycleConstant.firstScreen
lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
}

Comment thread
upupming marked this conversation as resolved.
// 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"
>
<text>
<raw-text
text="dark"
/>
</text>
</page>
`);
}
Comment thread
upupming marked this conversation as resolved.

// updateGlobalProps
{
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"
>
<text>
<raw-text
text="light"
/>
</text>
</page>
`);
}
Comment thread
upupming marked this conversation as resolved.
});
});
6 changes: 3 additions & 3 deletions packages/react/runtime/src/backgroundSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ export class BackgroundSnapshotInstance {
// return !!this.__parent;
// }

contains(child: BackgroundSnapshotInstance): boolean {
Comment thread
upupming marked this conversation as resolved.
return child.parentNode === this;
}
// contains(child: BackgroundSnapshotInstance): boolean {
// return child.parentNode === this;
// }

// This will be called in `lazy`/`Suspense`.
appendChild(child: BackgroundSnapshotInstance): void {
Expand Down
16 changes: 15 additions & 1 deletion packages/react/runtime/src/lynx/runWithForce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,23 @@
import { options } from 'preact';
import type { VNode } from 'preact';

import { COMPONENT, DIFF2, FORCE } from '../renderToOpcodes/constants.js';
import { COMPONENT, DIFF2, FORCE, ORIGINAL } from '../renderToOpcodes/constants.js';
import { __root } from '../root.js';

export function runWithForce(cb: () => void): void {
// 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) {
Comment thread
upupming marked this conversation as resolved.
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;
}
}
Comment thread
upupming marked this conversation as resolved.

const oldDiff = options[DIFF2];
options[DIFF2] = (vnode: VNode, oldVNode: VNode) => {
/* v8 ignore start */
Expand Down
2 changes: 0 additions & 2 deletions packages/react/runtime/src/lynx/suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const Suspense: FunctionComponent<{ children: VNode | VNode[]; fallback:
(__MAIN_THREAD__ ? createElementMainThread : createElementBackground) as typeof createElementBackground;
const childrenRef = useRef<BackgroundSnapshotInstance>();

// @ts-expect-error wrapper is a valid element type
const newChildren = __createElement('wrapper', {
ref: (bsi: BackgroundSnapshotInstance) => {
if (bsi) {
Expand All @@ -27,7 +26,6 @@ export const Suspense: FunctionComponent<{ children: VNode | VNode[]; fallback:
},
}, children);

// @ts-expect-error wrapper is a valid element type
const newFallback = __createElement('wrapper', {
ref: (bsi: BackgroundSnapshotInstance) => {
if (bsi && childrenRef.current) {
Expand Down
1 change: 1 addition & 0 deletions packages/react/runtime/src/renderToOpcodes/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const CHILDREN = '__k';
export const PARENT = '__';
export const MASK = '__m';
export const DOM = '__e';
export const ORIGINAL = '__v';

// Component properties
export const VNODE = '__v';
Expand Down
2 changes: 2 additions & 0 deletions packages/react/runtime/types/internal-preact.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ declare module 'preact' {
interface VNode {
/** _component */
__c?: Component | null;
/** _original */
__v?: number;
}

interface Component<P = {}, S = {}> {
Expand Down
35 changes: 35 additions & 0 deletions packages/react/testing-library/src/__tests__/render.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import '@testing-library/jest-dom';
import { test, expect } from 'vitest';
import { render } from '..';
import { createRef } from '@lynx-js/react';
import { Component } from 'preact';

test('renders view into page', async () => {
const ref = createRef();
Expand Down Expand Up @@ -259,3 +260,37 @@ describe('dynamic key in snapshot', () => {
`);
});
});

it('should handle keyed replacements', () => {
const actions = [];
class Comp extends Component {
componentDidMount() {
actions.push('mounted ' + this.props.i);
}
render() {
return <text>Hello</text>;
}
}

const App = props => {
return (
<view>
<Comp key={props.y} i={1} />
{false}
<Comp i={2} />
<Comp i={3} />
</view>
);
};

render(<App y='1' />);
expect(actions).to.deep.equal(['mounted 1', 'mounted 2', 'mounted 3']);

render(<App y='2' />);
expect(actions).to.deep.equal([
'mounted 1',
'mounted 2',
'mounted 3',
'mounted 1',
]);
});
Comment thread
upupming marked this conversation as resolved.
Loading
Loading