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

**BREAKING CHANGE**: Bump Preact from [10.24.0](https://github.com/preactjs/preact/commit/1807173df5e18b6b1a3cd667ee2a31af37f4282b) to [10.28.0](https://github.com/preactjs/preact/commit/f7693b72ecb4a40c66e6e47f54e2d4edc374c9f0), see diffs at [hzy/preact#6](https://github.com/hzy/preact/pull/6).
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