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/nice-needles-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/rspeedy": patch
---

Support CLI flag `--mode` to specify the build mode.
10 changes: 10 additions & 0 deletions .changeset/tired-drinks-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@lynx-js/react": minor
---

Fixed closure variable capture issue in effect hooks to prevent stale values and ensured proper execution order between refs, effects, and event handlers.

**Breaking Changes**:

- The execution timing of `ref` and `useEffect()` side effects has been moved forward. These effects will now execute before hydration is complete, rather than waiting for the main thread update to complete.
- For components inside `<list />`, `ref` callbacks will now be triggered during background thread rendering, regardless of component visibility. If your code depends on component visibility timing, use `main-thread:ref` instead of regular `ref`.
5 changes: 5 additions & 0 deletions .changeset/wild-sheep-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/tailwind-preset": patch
---

Support `hidden`, `no-underline` and `line-through` utilities.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ describe('delayedLifecycleEvents', () => {
"rLynxFirstScreen",
{
"jsReadyEventIdSwap": {},
"refPatch": "{}",
"root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_1"}]}",
},
],
Expand All @@ -45,7 +44,6 @@ describe('delayedLifecycleEvents', () => {
"rLynxFirstScreen",
{
"jsReadyEventIdSwap": {},
"refPatch": "{}",
"root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_1"}]}",
},
],
Expand Down
249 changes: 6 additions & 243 deletions packages/react/runtime/__test__/lifecycle.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,243 +42,21 @@ describe('useEffect', () => {
}

initGlobalSnapshotPatch();
let mtCallbacks = lynx.getNativeApp().callLepusMethod;
globalEnvManager.switchToBackground();

render(<Comp />, __root);
expect(callback).toHaveBeenCalledTimes(0);
expect(cleanUp).toHaveBeenCalledTimes(0);

expect(callback).toHaveBeenCalledTimes(0);
expect(mtCallbacks.mock.calls.length).toBe(1);
mtCallbacks.mock.calls[0][2]();
lynx.getNativeApp().callLepusMethod.mockClear();
expect(callback).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(0);

await waitSchedule();
expect(callback).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(0);

render(<Comp />, __root);
expect(callback).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(0);

expect(mtCallbacks.mock.calls.length).toBe(1);
mtCallbacks.mock.calls[0][2]();
lynx.getNativeApp().callLepusMethod.mockClear();

await waitSchedule();
expect(callback).toHaveBeenCalledTimes(2);
expect(cleanUp).toHaveBeenCalledTimes(1);
});

it('should call after main thread returns', async function() {
globalEnvManager.switchToBackground();

let mtCallbacks = lynx.getNativeApp().callLepusMethod.mock.calls;

const cleanUp = vi.fn();
const callback = vi.fn().mockImplementation(() => cleanUp);

function Comp() {
const [val, setVal] = useState(1);
useLayoutEffect(callback);
return <text>{val}</text>;
}

initGlobalSnapshotPatch();

render(<Comp />, __root);
render(<Comp />, __root);
render(<Comp />, __root);
expect(callback).toHaveBeenCalledTimes(0);
expect(cleanUp).toHaveBeenCalledTimes(0);

let mtCallback;
// expect(mtCallbacks.length).toEqual(3);
mtCallback = mtCallbacks.shift();
expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate);
expect(mtCallback[1]).toMatchInlineSnapshot(`
{
"data": "{"patchList":[{"id":3,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_2",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}",
"patchOptions": {
"reloadVersion": 0,
},
}
`);
mtCallback[2]();
await waitSchedule();
expect(callback).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(0);

expect(mtCallbacks.length).toEqual(2);
mtCallback = mtCallbacks.shift();
expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate);
expect(mtCallback[1]).toMatchInlineSnapshot(`
{
"data": "{"patchList":[{"id":4}]}",
"patchOptions": {
"reloadVersion": 0,
},
}
`);
mtCallback[2]();
await waitSchedule();
expect(callback).toHaveBeenCalledTimes(2);
expect(cleanUp).toHaveBeenCalledTimes(1);

expect(mtCallbacks.length).toEqual(1);
mtCallback = mtCallbacks.shift();
expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate);
expect(mtCallback[1]).toMatchInlineSnapshot(`
{
"data": "{"patchList":[{"id":5}]}",
"patchOptions": {
"reloadVersion": 0,
},
}
`);
mtCallback[2]();
await waitSchedule();
expect(callback).toHaveBeenCalledTimes(3);
expect(cleanUp).toHaveBeenCalledTimes(2);
});

it('change before hydration', async function() {
let setVal_;

const cleanUp = vi.fn();
const callback = vi.fn(() => {
return cleanUp;
});

function Comp() {
const [val, setVal] = useState(1);
setVal_ = setVal;
useLayoutEffect(callback);
return <text>{val}</text>;
}

// main thread render
{
__root.__jsx = <Comp />;
renderPage();
}

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

// background state change
{
setVal_(300);
await waitSchedule();
expect(lynx.getNativeApp().callLepusMethod).not.toBeCalled();
}

// background state change
{
setVal_(400);
await waitSchedule();
expect(lynx.getNativeApp().callLepusMethod).not.toBeCalled();
}

// hydrate
{
// LifecycleConstant.firstScreen
lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1);
expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot(
`"{"patchList":[{"snapshotPatch":[3,-3,0,400],"id":9}]}"`,
);
globalThis.__OnLifecycleEvent.mockClear();

await waitSchedule();
expect(callback).toHaveBeenCalledTimes(0);
expect(cleanUp).toHaveBeenCalledTimes(0);

// rLynxChange
globalEnvManager.switchToMainThread();
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
globalThis[rLynxChange[0]](rLynxChange[1]);
rLynxChange[2]();

await waitSchedule();
expect(callback).toHaveBeenCalledTimes(3);
expect(cleanUp).toHaveBeenCalledTimes(2);
}
});

it('cleanup function should delay when unmounts', async function() {
const cleanUp = vi.fn();
const callback = vi.fn(() => {
return cleanUp;
});

function A() {
useLayoutEffect(callback);
}

function Comp(props) {
return props.show && <A />;
}

// main thread render
{
__root.__jsx = <Comp show={false} />;
renderPage();
}

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

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

await waitSchedule();
expect(callback).toHaveBeenCalledTimes(0);
expect(cleanUp).toHaveBeenCalledTimes(0);

// rLynxChange
globalEnvManager.switchToMainThread();
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
rLynxChange[2]();
await waitSchedule();
}

// background unmount
{
globalEnvManager.switchToBackground();
lynx.getNativeApp().callLepusMethod.mockClear();
render(<Comp show={true} />, __root);
render(<Comp show={false} />, __root);
expect(callback).toHaveBeenCalledTimes(0);
expect(cleanUp).toHaveBeenCalledTimes(0);
}

{
expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(2);
let rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
rLynxChange[2]();
await waitSchedule();
expect(callback).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(0);

rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[1];
rLynxChange[2]();
await waitSchedule();
expect(callback).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(1);
}
});

it('throw', async function() {
Expand All @@ -287,8 +65,6 @@ describe('useEffect', () => {
const catchError = options[CATCH_ERROR];
options[CATCH_ERROR] = vi.fn();

let mtCallbacks = lynx.getNativeApp().callLepusMethod.mock.calls;

const callback = vi.fn().mockImplementation(() => {
throw '???';
});
Expand All @@ -303,23 +79,10 @@ describe('useEffect', () => {
render(<Comp />, __root);
render(<Comp />, __root);
render(<Comp />, __root);
expect(callback).toHaveBeenCalledTimes(0);
expect(callback).toHaveBeenCalledTimes(2);

let mtCallback;
expect(mtCallbacks.length).toEqual(3);
mtCallback = mtCallbacks.shift();
expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate);
expect(mtCallback[1]).toMatchInlineSnapshot(`
{
"data": "{"patchList":[{"id":14,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_4",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}",
"patchOptions": {
"reloadVersion": 0,
},
}
`);
mtCallback[2]();
await waitSchedule();
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledTimes(3);
expect(options[CATCH_ERROR]).toHaveBeenCalledWith('???', expect.anything());
options[CATCH_ERROR] = catchError;
});
Expand Down Expand Up @@ -359,7 +122,7 @@ describe('componentDidMount', () => {
expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate);
expect(mtCallback[1]).toMatchInlineSnapshot(`
{
"data": "{"patchList":[{"id":17,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_5",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}",
"data": "{"patchList":[{"id":6,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_3",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}",
"patchOptions": {
"reloadVersion": 0,
},
Expand Down Expand Up @@ -406,7 +169,7 @@ describe('componentDidMount', () => {
expect(mtCallback[0]).toEqual(LifecycleConstant.patchUpdate);
expect(mtCallback[1]).toMatchInlineSnapshot(`
{
"data": "{"patchList":[{"id":20,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_6",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}",
"data": "{"patchList":[{"id":9,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_4",2,0,null,3,3,3,0,1,1,2,3,null,1,1,2,null]}]}",
"patchOptions": {
"reloadVersion": 0,
},
Expand Down Expand Up @@ -739,7 +502,7 @@ describe('useState', () => {
await waitSchedule();
expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1);
expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot(
`"{"patchList":[{"id":36,"snapshotPatch":[3,-2,1,"abcd",3,-2,2,{"str":"efgh"}]}]}"`,
`"{"patchList":[{"id":25,"snapshotPatch":[3,-2,1,"abcd",3,-2,2,{"str":"efgh"}]}]}"`,
);
}
});
Expand Down Expand Up @@ -797,7 +560,7 @@ describe('useState', () => {
await waitSchedule();
expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1);
expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot(
`"{"patchList":[{"id":39,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_17",2,4,2,[false,{"str":"str"}],1,-1,2,null]}]}"`,
`"{"patchList":[{"id":28,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_15",2,4,2,[false,{"str":"str"}],1,-1,2,null]}]}"`,
);
}
});
Expand Down
Loading