diff --git a/.changeset/nice-needles-press.md b/.changeset/nice-needles-press.md
new file mode 100644
index 0000000000..1aff40f798
--- /dev/null
+++ b/.changeset/nice-needles-press.md
@@ -0,0 +1,5 @@
+---
+"@lynx-js/rspeedy": patch
+---
+
+Support CLI flag `--mode` to specify the build mode.
diff --git a/.changeset/tired-drinks-give.md b/.changeset/tired-drinks-give.md
new file mode 100644
index 0000000000..52f5229d3f
--- /dev/null
+++ b/.changeset/tired-drinks-give.md
@@ -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 `
`, `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`.
diff --git a/.changeset/wild-sheep-dream.md b/.changeset/wild-sheep-dream.md
new file mode 100644
index 0000000000..a71a91af9b
--- /dev/null
+++ b/.changeset/wild-sheep-dream.md
@@ -0,0 +1,5 @@
+---
+"@lynx-js/tailwind-preset": patch
+---
+
+Support `hidden`, `no-underline` and `line-through` utilities.
diff --git a/packages/react/runtime/__test__/delayed-lifecycle-events.test.jsx b/packages/react/runtime/__test__/delayed-lifecycle-events.test.jsx
index b5694e293d..2b7dbeed9e 100644
--- a/packages/react/runtime/__test__/delayed-lifecycle-events.test.jsx
+++ b/packages/react/runtime/__test__/delayed-lifecycle-events.test.jsx
@@ -31,7 +31,6 @@ describe('delayedLifecycleEvents', () => {
"rLynxFirstScreen",
{
"jsReadyEventIdSwap": {},
- "refPatch": "{}",
"root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_1"}]}",
},
],
@@ -45,7 +44,6 @@ describe('delayedLifecycleEvents', () => {
"rLynxFirstScreen",
{
"jsReadyEventIdSwap": {},
- "refPatch": "{}",
"root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_1"}]}",
},
],
diff --git a/packages/react/runtime/__test__/lifecycle.test.jsx b/packages/react/runtime/__test__/lifecycle.test.jsx
index bfb64ad1e5..a7684a8150 100644
--- a/packages/react/runtime/__test__/lifecycle.test.jsx
+++ b/packages/react/runtime/__test__/lifecycle.test.jsx
@@ -42,243 +42,21 @@ describe('useEffect', () => {
}
initGlobalSnapshotPatch();
- let mtCallbacks = lynx.getNativeApp().callLepusMethod;
globalEnvManager.switchToBackground();
render(, __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(, __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 {val};
- }
-
- initGlobalSnapshotPatch();
-
- render(, __root);
render(, __root);
- render(, __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 {val};
- }
-
- // main thread render
- {
- __root.__jsx = ;
- renderPage();
- }
-
- // background render
- {
- globalEnvManager.switchToBackground();
- render(, __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 && ;
- }
-
- // main thread render
- {
- __root.__jsx = ;
- renderPage();
- }
-
- // background render
- {
- globalEnvManager.switchToBackground();
- render(, __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(, __root);
- render(, __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() {
@@ -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 '???';
});
@@ -303,23 +79,10 @@ describe('useEffect', () => {
render(, __root);
render(, __root);
render(, __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;
});
@@ -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,
},
@@ -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,
},
@@ -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"}]}]}"`,
);
}
});
@@ -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]}]}"`,
);
}
});
diff --git a/packages/react/runtime/__test__/lifecycle/reload.test.jsx b/packages/react/runtime/__test__/lifecycle/reload.test.jsx
index eb7203c291..f811d42173 100644
--- a/packages/react/runtime/__test__/lifecycle/reload.test.jsx
+++ b/packages/react/runtime/__test__/lifecycle/reload.test.jsx
@@ -301,7 +301,6 @@ describe('reload', () => {
[
"rLynxFirstScreen",
{
- "refPatch": "{}",
"root": "{"id":-9,"type":"root","children":[{"id":-13,"type":"__Card__:__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-10,"type":"__Card__:__snapshot_a94a8_test_3","children":[{"id":-15,"type":null,"values":["Enjoy"]}]},{"id":-11,"type":"__Card__:__snapshot_a94a8_test_4","children":[{"id":-16,"type":null,"values":["World"]}]},{"id":-12,"type":"wrapper","children":[{"id":-14,"type":"__Card__:__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}",
},
],
@@ -711,7 +710,6 @@ describe('reload', () => {
[
"rLynxFirstScreen",
{
- "refPatch": "{}",
"root": "{"id":-9,"type":"root","children":[{"id":-15,"type":"__Card__:__snapshot_a94a8_test_5","children":[{"id":-13,"type":"__Card__:__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-10,"type":"__Card__:__snapshot_a94a8_test_3","children":[{"id":-16,"type":null,"values":["Enjoy"]}]},{"id":-11,"type":"__Card__:__snapshot_a94a8_test_4","children":[{"id":-17,"type":null,"values":["World"]}]},{"id":-12,"type":"wrapper","children":[{"id":-14,"type":"__Card__:__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}]}",
},
],
@@ -1314,7 +1312,6 @@ describe('firstScreenSyncTiming - jsReady', () => {
"-8": -16,
"-9": -17,
},
- "refPatch": "{}",
"root": "{"id":-17,"type":"root","children":[{"id":-21,"type":"__Card__:__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-18,"type":"__Card__:__snapshot_a94a8_test_3","children":[{"id":-23,"type":null,"values":["Hello 2"]}]},{"id":-19,"type":"__Card__:__snapshot_a94a8_test_4","children":[{"id":-24,"type":null,"values":["World"]}]},{"id":-20,"type":"wrapper","children":[{"id":-22,"type":"__Card__:__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}",
},
],
@@ -1518,7 +1515,6 @@ describe('firstScreenSyncTiming - jsReady', () => {
"-5": -13,
"-9": -17,
},
- "refPatch": "{}",
"root": "{"id":-17,"type":"root","children":[{"id":-21,"type":"__Card__:__snapshot_a94a8_test_7","children":[{"id":-18,"type":"__Card__:__snapshot_a94a8_test_8","values":[{"item-key":0}],"children":[{"id":-22,"type":"__Card__:__snapshot_a94a8_test_6","values":["a"]}]},{"id":-19,"type":"__Card__:__snapshot_a94a8_test_8","values":[{"item-key":1}],"children":[{"id":-23,"type":"__Card__:__snapshot_a94a8_test_6","values":["b"]}]},{"id":-20,"type":"__Card__:__snapshot_a94a8_test_8","values":[{"item-key":2}],"children":[{"id":-24,"type":"__Card__:__snapshot_a94a8_test_6","values":["c"]}]}]}]}",
},
],
@@ -1683,7 +1679,6 @@ describe('firstScreenSyncTiming - jsReady', () => {
"-2": -10,
"-6": -14,
},
- "refPatch": "{}",
"root": "{"id":-10,"type":"root","children":[{"id":-14,"type":"__Card__:__snapshot_a94a8_test_9","children":[{"id":-11,"type":"__Card__:__snapshot_a94a8_test_10","values":[{"item-key":0}],"children":[{"id":-15,"type":"__Card__:__snapshot_a94a8_test_6","values":["a"]}]},{"id":-12,"type":"__Card__:__snapshot_a94a8_test_10","values":[{"item-key":1}],"children":[{"id":-16,"type":"__Card__:__snapshot_a94a8_test_6","values":["b"]}]},{"id":-13,"type":"__Card__:__snapshot_a94a8_test_10","values":[{"item-key":2}],"children":[{"id":-17,"type":"__Card__:__snapshot_a94a8_test_6","values":["c"]}]}]}]}",
},
],
@@ -1877,7 +1872,6 @@ describe('firstScreenSyncTiming - jsReady', () => {
"rLynxFirstScreen",
{
"jsReadyEventIdSwap": {},
- "refPatch": "{}",
"root": "{"id":-17,"type":"root","children":[{"id":-21,"type":"__Card__:__snapshot_a94a8_test_2","values":[{"dataX":"WorldX"}],"children":[{"id":-18,"type":"__Card__:__snapshot_a94a8_test_3","children":[{"id":-23,"type":null,"values":["Hello 2"]}]},{"id":-19,"type":"__Card__:__snapshot_a94a8_test_4","children":[{"id":-24,"type":null,"values":["World"]}]},{"id":-20,"type":"wrapper","children":[{"id":-22,"type":"__Card__:__snapshot_a94a8_test_1","values":[{"attr":{"dataX":"WorldX"}}]}]}]}]}",
},
],
diff --git a/packages/react/runtime/__test__/page.test.jsx b/packages/react/runtime/__test__/page.test.jsx
index 1a94120aec..d08f41c0f3 100644
--- a/packages/react/runtime/__test__/page.test.jsx
+++ b/packages/react/runtime/__test__/page.test.jsx
@@ -1,9 +1,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
import { __page } from '../src/snapshot';
-import { elementTree } from './utils/nativeMethod';
import { globalEnvManager } from './utils/envManager';
+import { elementTree } from './utils/nativeMethod';
+import { useRef, useState } from '../src/index';
import { __root } from '../src/root';
-import { useState, useRef } from '../src/index';
beforeEach(() => {
globalEnvManager.resetEnv();
@@ -77,7 +78,7 @@ describe('support element attributes', () => {
"bindEvent:tap": "-1:0:bindtap",
}
}
- has-react-ref={true}
+ react-ref--1-0={1}
>
@@ -315,6 +316,7 @@ describe('support element attributes', () => {
expect(__root.__element_root).toMatchInlineSnapshot(`
{
"-5": -8,
"-6": -9,
},
- "refPatch": "{}",
"root": "{"id":-7,"type":"root","children":[{"id":-8,"type":"__Card__:__snapshot_a94a8_test_12","children":[{"id":-9,"type":"__Card__:__snapshot_a94a8_test_11","values":["-9:0:"]}]}]}",
},
],
@@ -1272,7 +1271,6 @@ describe('call `root.render()` async', () => {
"rLynxFirstScreen",
{
"jsReadyEventIdSwap": {},
- "refPatch": "{}",
"root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_14","children":[{"id":-3,"type":"__Card__:__snapshot_a94a8_test_13","values":["-3:0:"]}]}]}",
},
],
diff --git a/packages/react/runtime/__test__/snapshot/ref.test.jsx b/packages/react/runtime/__test__/snapshot/ref.test.jsx
index 66e1251f8c..a8b1f1fb09 100644
--- a/packages/react/runtime/__test__/snapshot/ref.test.jsx
+++ b/packages/react/runtime/__test__/snapshot/ref.test.jsx
@@ -6,8 +6,7 @@
import { render } from 'preact';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
-import { Component, createRef, root, useState } from '../../src/index';
-import { delayedLifecycleEvents } from '../../src/lifecycle/event/delayLifecycleEvents';
+import { Component, createRef, useState } from '../../src/index';
import { clearCommitTaskId, replaceCommitHook } from '../../src/lifecycle/patch/commit';
import { injectUpdateMainThread } from '../../src/lifecycle/patch/updateMainThread';
import { __pendingListUpdates } from '../../src/list';
@@ -102,10 +101,10 @@ describe('element ref', () => {
>
@@ -116,8 +115,7 @@ describe('element ref', () => {
"rLynxFirstScreen",
{
"jsReadyEventIdSwap": {},
- "refPatch": "{"-2:0:":3,"-2:1:":4}",
- "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_3","values":["-2:0:","-2:1:"]}]}",
+ "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_3","values":["react-ref--2-0","react-ref--2-1"]}]}",
},
],
]
@@ -128,50 +126,33 @@ describe('element ref', () => {
{
globalEnvManager.switchToBackground();
render(, __root);
- expect(ref1).not.toBeCalled();
- expect(ref2.current).toBe(null);
- }
-
- // 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":[],"id":2}]}"`,
- );
- lynx.getNativeApp().callLepusMethod.mock.calls[0][2]();
- await waitSchedule();
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- {
- "selectUniqueID": [Function],
- "uid": 3,
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
`);
- expect(ref2).toMatchInlineSnapshot(`
- {
- "current": {
- "selectUniqueID": [Function],
- "uid": 4,
- },
+ expect(ref2.current).toMatchInlineSnapshot(`
+ RefProxy {
+ "refAttr": [
+ 2,
+ 1,
+ ],
+ "task": undefined,
}
`);
-
- // rLynxChange
- globalEnvManager.switchToMainThread();
- globalThis.__OnLifecycleEvent.mockClear();
- const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
- globalThis[rLynxChange[0]](rLynxChange[1]);
- expect(globalThis.__OnLifecycleEvent).not.toBeCalled();
}
});
- it('insert', async function() {
+ it('should trigger ref when insert node', async function() {
const ref1 = vi.fn();
const ref2 = createRef();
@@ -219,69 +200,49 @@ describe('element ref', () => {
render(, __root);
expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1);
expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot(
- `"{"patchList":[{"id":3,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_4",2,4,2,[3,4],1,-1,2,null]}]}"`,
+ `"{"patchList":[{"id":3,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_4",2,4,2,[1,1],1,-1,2,null]}]}"`,
);
}
// rLynxChange
{
globalEnvManager.switchToMainThread();
- globalThis.__OnLifecycleEvent.mockClear();
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
globalThis[rLynxChange[0]](rLynxChange[1]);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
rLynxChange[2]();
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
- [
- [
- [
- "rLynxRef",
- {
- "commitTaskId": 3,
- "refPatch": "{"2:0:":7,"2:1:":8}",
- },
- ],
- ],
- ]
- `);
}
// ref
{
globalEnvManager.switchToBackground();
- await waitSchedule();
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- {
- "selectUniqueID": [Function],
- "uid": 7,
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
`);
expect(ref2).toMatchInlineSnapshot(`
{
- "current": {
- "selectUniqueID": [Function],
- "uid": 8,
+ "current": RefProxy {
+ "refAttr": [
+ 2,
+ 1,
+ ],
+ "task": undefined,
},
}
`);
}
-
- {
- globalEnvManager.switchToBackground();
- lynx.getNativeApp().callLepusMethod.mockClear();
- render(, __root);
- expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1);
- expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot(
- `"{"patchList":[{"id":4,"snapshotPatch":[3,2,0,3,3,2,1,4]}]}"`,
- );
- }
});
- it('remove', async function() {
+ it('should trigger ref when remove node', async function() {
const ref1 = vi.fn();
const ref2 = createRef();
@@ -334,35 +295,9 @@ describe('element ref', () => {
);
}
- // rLynxChange
- {
- globalEnvManager.switchToMainThread();
- globalThis.__OnLifecycleEvent.mockClear();
- const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
- globalThis[rLynxChange[0]](rLynxChange[1]);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
- [
- [
- [
- "rLynxRef",
- {
- "commitTaskId": 3,
- "refPatch": "{"-2:0:":null,"-2:1:":null}",
- },
- ],
- ],
- ]
- `);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- globalThis.__OnLifecycleEvent.mockClear();
- rLynxChange[2]();
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`[]`);
- }
-
// ref patch
{
globalEnvManager.switchToBackground();
- await waitSchedule();
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
@@ -374,7 +309,7 @@ describe('element ref', () => {
}
});
- it('remove with cleanup function', async function() {
+ it('should trigger ref when remove node with cleanup function', async function() {
const cleanup = vi.fn();
const ref1 = vi.fn(() => {
return cleanup;
@@ -428,33 +363,9 @@ describe('element ref', () => {
);
}
- // rLynxChange
- {
- globalEnvManager.switchToMainThread();
- globalThis.__OnLifecycleEvent.mockClear();
- const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
- globalThis[rLynxChange[0]](rLynxChange[1]);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- rLynxChange[2]();
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
- [
- [
- [
- "rLynxRef",
- {
- "commitTaskId": 3,
- "refPatch": "{"-2:0:":null}",
- },
- ],
- ],
- ]
- `);
- }
-
// ref patch
{
globalEnvManager.switchToBackground();
- await waitSchedule();
expect(ref1).not.toBeCalled();
expect(cleanup.mock.calls).toMatchInlineSnapshot(`
[
@@ -464,7 +375,7 @@ describe('element ref', () => {
}
});
- it('callback should ref and unref deeply', async () => {
+ it('should trigger ref when ref and unref deeply', async () => {
const ref1 = [vi.fn(), vi.fn(), vi.fn()];
const ref2 = vi.fn();
let _setShow;
@@ -509,16 +420,15 @@ describe('element ref', () => {
globalThis.__OnLifecycleEvent.mockClear();
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
globalThis[rLynxChange[0]](rLynxChange[1]);
- expect(globalThis.__OnLifecycleEvent).not.toBeCalled();
}
ref1.forEach(ref => {
expect(ref).toHaveBeenCalledWith(expect.objectContaining({
- uid: expect.any(Number),
+ refAttr: expect.any(Array),
}));
});
expect(ref2).toHaveBeenCalledWith(expect.objectContaining({
- uid: expect.any(Number),
+ refAttr: expect.any(Array),
}));
ref1.forEach(ref => ref.mockClear());
ref2.mockClear();
@@ -531,39 +441,15 @@ describe('element ref', () => {
await waitSchedule();
}
- // rLynxChange
- {
- globalEnvManager.switchToMainThread();
- globalThis.__OnLifecycleEvent.mockClear();
- const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
- globalThis[rLynxChange[0]](rLynxChange[1]);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
- [
- [
- [
- "rLynxRef",
- {
- "commitTaskId": 3,
- "refPatch": "{"-2:0:":null,"-3:0:":null,"-4:0:":null,"-5:0:":null}",
- },
- ],
- ],
- ]
- `);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- rLynxChange[2]();
- }
-
- // ref patch
+ // ref check
{
globalEnvManager.switchToBackground();
- await waitSchedule();
ref1.forEach(ref => expect(ref).toHaveBeenCalledWith(null));
expect(ref2).toHaveBeenCalledWith(null);
}
});
- it('hydrate', async function() {
+ it('should trigger ref when ref is null in the first screen', async function() {
const ref1 = createRef();
const ref2 = createRef();
const ref3 = vi.fn();
@@ -591,10 +477,14 @@ describe('element ref', () => {
>
+
+
-
-
`);
@@ -604,8 +494,7 @@ describe('element ref', () => {
"rLynxFirstScreen",
{
"jsReadyEventIdSwap": {},
- "refPatch": "{"-2:0:":23}",
- "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_9","values":["-2:0:",null,null]}]}",
+ "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_9","values":["react-ref--2-0","react-ref--2-1","react-ref--2-2"]}]}",
},
],
]
@@ -617,6 +506,30 @@ describe('element ref', () => {
globalEnvManager.switchToBackground();
render(, __root);
lynx.getNativeApp().callLepusMethod.mockClear();
+
+ expect(ref1.current).toBeNull();
+ expect(ref2.current).toMatchInlineSnapshot(`
+ RefProxy {
+ "refAttr": [
+ 2,
+ 1,
+ ],
+ "task": undefined,
+ }
+ `);
+ expect(ref3.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ RefProxy {
+ "refAttr": [
+ 2,
+ 2,
+ ],
+ "task": undefined,
+ },
+ ],
+ ]
+ `);
}
// hydrate
@@ -625,135 +538,36 @@ describe('element ref', () => {
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,-2,0,null,3,-2,1,13,3,-2,2,14],"id":2}]}"`,
+ `"{"patchList":[{"snapshotPatch":[3,-2,0,null],"id":2}]}"`,
);
- expect(ref1.current).toBeNull();
- expect(ref2.current).toBeNull();
- expect(ref3).not.toBeCalled();
-
// rLynxChange
globalEnvManager.switchToMainThread();
- globalThis.__OnLifecycleEvent.mockClear();
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
globalThis[rLynxChange[0]](rLynxChange[1]);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
rLynxChange[2]();
+ // Keep "-2:0" exists even if it is set to null. This is for the first screen.
expect(__root.__element_root).toMatchInlineSnapshot(`
`);
-
- // ref patch
- {
- globalEnvManager.switchToBackground();
- await waitSchedule();
- expect(ref1.current).toBeNull();
- expect(ref2).toMatchInlineSnapshot(`
- {
- "current": {
- "selectUniqueID": [Function],
- "uid": 24,
- },
- }
- `);
- expect(ref3.mock.calls).toMatchInlineSnapshot(`
- [
- [
- {
- "selectUniqueID": [Function],
- "uid": 25,
- },
- ],
- ]
- `);
- }
- }
- });
-
- it('change before hydration', async function() {
- const ref1 = createRef();
- const ref2 = createRef();
-
- class Comp extends Component {
- x = 'x';
- render() {
- return (
-
-
-
-
- );
- }
- }
-
- // main thread render
- {
- __root.__jsx = ;
- renderPage();
- }
-
- // background render
- {
- globalEnvManager.switchToBackground();
- render(, __root);
- lynx.getNativeApp().callLepusMethod.mockClear();
- }
-
- // background state change
- {
- render(, __root);
- 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,-2,0,null,3,-2,1,16],"id":3}]}"`,
- );
- globalThis.__OnLifecycleEvent.mockClear();
-
- // rLynxChange
- globalEnvManager.switchToMainThread();
- const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
- globalThis[rLynxChange[0]](rLynxChange[1]);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- rLynxChange[2]();
-
- // ref patch
- {
- globalEnvManager.switchToBackground();
- await waitSchedule();
- expect(ref1.current).toBeNull();
- expect(ref2).toMatchInlineSnapshot(`
- {
- "current": {
- "selectUniqueID": [Function],
- "uid": 29,
- },
- }
- `);
- }
}
});
- it('wrong ref type', async function() {
+ it('should throw error when ref type is wrong', async function() {
let ref1 = 1;
class Comp extends Component {
@@ -791,7 +605,7 @@ describe('element ref', () => {
}
});
- it('update', async function() {
+ it('should trigger ref when ref object is updated', async function() {
const cleanup = vi.fn();
let ref1 = vi.fn(() => {
return cleanup;
@@ -800,7 +614,6 @@ describe('element ref', () => {
let ref3 = createRef();
class Comp extends Component {
- x = 'x';
render() {
return (
@@ -850,52 +663,34 @@ describe('element ref', () => {
render(, __root);
expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1);
expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot(
- `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,0,20,3,-2,1,21,3,-2,2,null]}]}"`,
+ `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,2,null]}]}"`,
);
}
- // rLynxChange
- {
- globalEnvManager.switchToMainThread();
- globalThis.__OnLifecycleEvent.mockClear();
- const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
- globalThis[rLynxChange[0]](rLynxChange[1]);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- rLynxChange[2]();
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
- [
- [
- [
- "rLynxRef",
- {
- "commitTaskId": 3,
- "refPatch": "{"-2:0:":32,"-2:1:":33}",
- },
- ],
- ],
- ]
- `);
- }
-
// ref
{
globalEnvManager.switchToBackground();
- await waitSchedule();
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- {
- "selectUniqueID": [Function],
- "uid": 32,
+ RefProxy {
+ "refAttr": [
+ -2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
`);
expect(ref2).toMatchInlineSnapshot(`
{
- "current": {
- "selectUniqueID": [Function],
- "uid": 33,
+ "current": RefProxy {
+ "refAttr": [
+ -2,
+ 1,
+ ],
+ "task": undefined,
},
}
`);
@@ -909,24 +704,14 @@ describe('element ref', () => {
expect(oldRef3.current).toBeNull();
}
});
-});
-describe('element ref in spread', () => {
- it('insert', async function() {
- const ref1 = vi.fn();
- const ref2 = createRef();
- let spread1 = {};
- const spread2 = { ref: ref2 };
+ it('should work when using ref along with other attributes', async function() {
+ const ref = createRef();
+ const attr1 = 1;
class Comp extends Component {
- x = 'x';
render() {
- return (
-
-
-
-
- );
+ return ;
}
}
@@ -934,59 +719,107 @@ describe('element ref in spread', () => {
{
__root.__jsx = ;
renderPage();
- expect(__root.__element_root).toMatchInlineSnapshot(`
-
-
-
-
-
-
- `);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
- [
- [
- [
- "rLynxFirstScreen",
- {
- "jsReadyEventIdSwap": {},
- "refPatch": "{"-2:1:ref":38}",
- "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_13","values":[{},{"ref":"-2:1:ref"}]}]}",
- },
- ],
- ],
- ]
- `);
}
// background render
{
globalEnvManager.switchToBackground();
render(, __root);
- lynx.getNativeApp().callLepusMethod.mockClear();
+ expect(ref.current).toMatchInlineSnapshot(`
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
+ }
+ `);
}
+ });
- // hydrate
+ // NOT working for now
+ it.skip('should work when using error boundary with ref', async function() {
+ const ref = vi.fn(() => {
+ throw new Error('error in ref');
+ });
+ const errorHandler = vi.fn();
+
+ class Comp extends Component {
+ state = { hasError: false };
+
+ componentDidCatch(error, info) {
+ errorHandler(error, info);
+ this.setState({ hasError: true });
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return Error occurred;
+ }
+ return ;
+ }
+ }
+
+ // main thread render
{
- // LifecycleConstant.firstScreen
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- globalThis.__OnLifecycleEvent.mockClear();
+ __root.__jsx = ;
+ renderPage();
+ }
- // rLynxChange
- globalEnvManager.switchToMainThread();
- const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
- globalThis[rLynxChange[0]](rLynxChange[1]);
+ // background render
+ {
+ globalEnvManager.switchToBackground();
+ render(, __root);
+ expect(ref.current).toMatchInlineSnapshot(`undefined`);
+
+ expect(errorHandler).toHaveBeenCalledTimes(1);
+ }
+ });
+});
+
+describe('element ref in spread', () => {
+ it('should trigger ref when insert ref into spread', async function() {
+ const ref1 = vi.fn();
+ const ref2 = createRef();
+ let spread1 = {};
+ const spread2 = { ref: ref2 };
+
+ class Comp extends Component {
+ x = 'x';
+ render() {
+ return (
+
+
+
+
+ );
+ }
+ }
+
+ // main thread render
+ {
+ __root.__jsx = ;
+ renderPage();
+ expect(__root.__element_root).toMatchInlineSnapshot(`
+
+
+
+
+
+
+ `);
expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
[
[
[
- "rLynxRef",
+ "rLynxFirstScreen",
{
- "commitTaskId": 2,
- "refPatch": "{"-2:1:ref":38}",
+ "jsReadyEventIdSwap": {},
+ "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_15","values":[{},{"ref":"react-ref--2-1"}]}]}",
},
],
],
@@ -994,15 +827,36 @@ describe('element ref in spread', () => {
`);
}
- // ref
+ // background render
{
+ globalEnvManager.switchToBackground();
+ render(, __root);
+ lynx.getNativeApp().callLepusMethod.mockClear();
+ }
+
+ // hydrate
+ {
+ // LifecycleConstant.firstScreen
lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
+ globalThis.__OnLifecycleEvent.mockClear();
+
+ // rLynxChange
+ globalEnvManager.switchToMainThread();
+ const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
+ globalThis[rLynxChange[0]](rLynxChange[1]);
+ }
+
+ // ref
+ {
expect(ref1.mock.calls).toMatchInlineSnapshot(`[]`);
expect(ref2).toMatchInlineSnapshot(`
{
- "current": {
- "selectUniqueID": [Function],
- "uid": 38,
+ "current": RefProxy {
+ "refAttr": [
+ 2,
+ 1,
+ ],
+ "task": undefined,
},
}
`);
@@ -1016,43 +870,44 @@ describe('element ref in spread', () => {
render(, __root);
expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1);
expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot(
- `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,0,{"ref":23}]}]}"`,
+ `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,0,{"ref":1}]}]}"`,
);
}
// rLynxChange
{
globalEnvManager.switchToMainThread();
- globalThis.__OnLifecycleEvent.mockClear();
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
globalThis[rLynxChange[0]](rLynxChange[1]);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
rLynxChange[2]();
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
- [
- [
- [
- "rLynxRef",
- {
- "commitTaskId": 3,
- "refPatch": "{"-2:0:ref":37}",
- },
- ],
- ],
- ]
+ expect(__root.__element_root).toMatchInlineSnapshot(`
+
+
+
+
+
+
`);
}
// ref
{
globalEnvManager.switchToBackground();
- await waitSchedule();
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- {
- "selectUniqueID": [Function],
- "uid": 37,
+ RefProxy {
+ "refAttr": [
+ -2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
@@ -1060,7 +915,7 @@ describe('element ref in spread', () => {
}
});
- it('remove', async function() {
+ it('should trigger ref when remove ref from spread', async function() {
const ref1 = vi.fn();
const ref2 = createRef();
let spread1 = { ref: ref1 };
@@ -1099,18 +954,24 @@ describe('element ref in spread', () => {
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- {
- "selectUniqueID": [Function],
- "uid": 43,
+ RefProxy {
+ "refAttr": [
+ 3,
+ 0,
+ ],
+ "task": undefined,
},
],
]
`);
expect(ref2).toMatchInlineSnapshot(`
{
- "current": {
- "selectUniqueID": [Function],
- "uid": 42,
+ "current": RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
}
`);
@@ -1123,8 +984,6 @@ describe('element ref in spread', () => {
// ref
{
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- globalThis.__OnLifecycleEvent.mockClear();
ref1.mockClear();
}
@@ -1143,16 +1002,13 @@ describe('element ref in spread', () => {
// rLynxChange
{
globalEnvManager.switchToMainThread();
- globalThis.__OnLifecycleEvent.mockClear();
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
globalThis[rLynxChange[0]](rLynxChange[1]);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
rLynxChange[2]();
}
// ref
{
- await waitSchedule();
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
@@ -1164,7 +1020,7 @@ describe('element ref in spread', () => {
}
});
- it('update', async function() {
+ it('should trigger ref when update ref in spread', async function() {
let ref1 = vi.fn();
let ref2 = createRef();
let ref3 = createRef();
@@ -1206,26 +1062,35 @@ describe('element ref in spread', () => {
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- {
- "selectUniqueID": [Function],
- "uid": 46,
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
`);
expect(ref2).toMatchInlineSnapshot(`
{
- "current": {
- "selectUniqueID": [Function],
- "uid": 47,
+ "current": RefProxy {
+ "refAttr": [
+ 2,
+ 1,
+ ],
+ "task": undefined,
},
}
`);
expect(ref3).toMatchInlineSnapshot(`
{
- "current": {
- "selectUniqueID": [Function],
- "uid": 48,
+ "current": RefProxy {
+ "refAttr": [
+ 2,
+ 2,
+ ],
+ "task": undefined,
},
}
`);
@@ -1242,8 +1107,6 @@ describe('element ref in spread', () => {
// ref
{
globalEnvManager.switchToBackground();
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- globalThis.__OnLifecycleEvent.mockClear();
ref1.mockClear();
}
@@ -1262,52 +1125,43 @@ describe('element ref in spread', () => {
render(, __root);
expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1);
expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot(
- `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,0,{"ref":29},3,-2,1,{"ref":30},3,-2,2,{}]}]}"`,
+ `"{"patchList":[{"id":3,"snapshotPatch":[3,-2,2,{}]}]}"`,
);
}
// rLynxChange
{
globalEnvManager.switchToMainThread();
- globalThis.__OnLifecycleEvent.mockClear();
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
globalThis[rLynxChange[0]](rLynxChange[1]);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
rLynxChange[2]();
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
- [
- [
- [
- "rLynxRef",
- {
- "commitTaskId": 3,
- "refPatch": "{"-2:0:ref":46,"-2:1:ref":47}",
- },
- ],
- ],
- ]
- `);
+ lynx.getNativeApp().callLepusMethod.mockClear();
}
// ref
{
globalEnvManager.switchToBackground();
- await waitSchedule();
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- {
- "selectUniqueID": [Function],
- "uid": 46,
+ RefProxy {
+ "refAttr": [
+ -2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
`);
expect(ref2).toMatchInlineSnapshot(`
{
- "current": {
- "selectUniqueID": [Function],
- "uid": 47,
+ "current": RefProxy {
+ "refAttr": [
+ -2,
+ 1,
+ ],
+ "task": undefined,
},
}
`);
@@ -1320,14 +1174,50 @@ describe('element ref in spread', () => {
`);
expect(oldRef2.current).toBeNull();
expect(oldRef3.current).toBeNull();
+ ref1.mockClear();
+ }
+
+ // update ref
+ {
+ ref3 = createRef();
+ spread3 = { ref: ref3 };
+ render(, __root);
+ expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1);
+ expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot(
+ `"{"patchList":[{"id":4,"snapshotPatch":[3,-2,2,{"ref":1}]}]}"`,
+ );
+ expect(ref1.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ null,
+ ],
+ [
+ RefProxy {
+ "refAttr": [
+ -2,
+ 0,
+ ],
+ "task": undefined,
+ },
+ ],
+ ]
+ `);
+ expect(ref3.current).toMatchInlineSnapshot(`
+ RefProxy {
+ "refAttr": [
+ -2,
+ 2,
+ ],
+ "task": undefined,
+ }
+ `);
}
});
});
describe('element ref in list', () => {
- it('hydrate', async function() {
+ it('should trigger ref in list', async function() {
const refs = [createRef(), createRef(), createRef()];
- const signs = [0, 0, 0];
class ListItem extends Component {
render() {
@@ -1367,17 +1257,17 @@ describe('element ref in list', () => {
{
"item-key": 0,
"position": 0,
- "type": "__Card__:__snapshot_a94a8_test_19",
+ "type": "__Card__:__snapshot_a94a8_test_21",
},
{
"item-key": 1,
"position": 1,
- "type": "__Card__:__snapshot_a94a8_test_19",
+ "type": "__Card__:__snapshot_a94a8_test_21",
},
{
"item-key": 2,
"position": 2,
- "type": "__Card__:__snapshot_a94a8_test_19",
+ "type": "__Card__:__snapshot_a94a8_test_21",
},
],
"removeAction": [],
@@ -1395,140 +1285,59 @@ describe('element ref in list', () => {
globalEnvManager.switchToBackground();
render(, __root);
lynx.getNativeApp().callLepusMethod.mockClear();
- }
-
- // hydrate
- {
- // LifecycleConstant.firstScreen
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- globalThis.__OnLifecycleEvent.mockClear();
- // rLynxChange
- globalEnvManager.switchToMainThread();
- const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
- globalThis[rLynxChange[0]](rLynxChange[1]);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`[]`);
- }
-
- // list render item 1 & 2
- {
- signs[0] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 0);
- signs[1] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 1);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
- [
- [
- [
- "rLynxRef",
- {
- "commitTaskId": undefined,
- "refPatch": "{"-4:0:":52}",
- },
- ],
- ],
- [
- [
- "rLynxRef",
- {
- "commitTaskId": undefined,
- "refPatch": "{"-6:0:":54}",
- },
- ],
- ],
- ]
- `);
- globalEnvManager.switchToBackground();
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[1]);
- globalThis.__OnLifecycleEvent.mockClear();
expect(refs[0]).toMatchInlineSnapshot(`
{
- "current": {
- "selectUniqueID": [Function],
- "uid": 52,
+ "current": RefProxy {
+ "refAttr": [
+ 4,
+ 0,
+ ],
+ "task": undefined,
},
}
`);
expect(refs[1]).toMatchInlineSnapshot(`
{
- "current": {
- "selectUniqueID": [Function],
- "uid": 54,
- },
- }
- `);
- expect(refs[2].current).toBeNull();
- }
-
- // list enqueue item 1 & render item 3
- {
- globalEnvManager.switchToMainThread();
- elementTree.triggerEnqueueComponent(__root.childNodes[0].__elements[0], signs[0]);
- elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 2);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
- [
- [
- [
- "rLynxRef",
- {
- "commitTaskId": undefined,
- "refPatch": "{"-4:0:":null,"-8:0:":52}",
- },
+ "current": RefProxy {
+ "refAttr": [
+ 6,
+ 0,
],
- ],
- ]
- `);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- globalThis.__OnLifecycleEvent.mockClear();
- expect(refs[0].current).toBeNull();
- expect(refs[1]).toMatchInlineSnapshot(`
- {
- "current": {
- "selectUniqueID": [Function],
- "uid": 54,
+ "task": undefined,
},
}
`);
expect(refs[2]).toMatchInlineSnapshot(`
{
- "current": {
- "selectUniqueID": [Function],
- "uid": 52,
+ "current": RefProxy {
+ "refAttr": [
+ 8,
+ 0,
+ ],
+ "task": undefined,
},
}
`);
}
-
- // list enqueue item 2 & render item 2
- {
- globalEnvManager.switchToMainThread();
- elementTree.triggerEnqueueComponent(__root.childNodes[0].__elements[0], signs[1]);
- elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 1);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`[]`);
- }
});
+});
- it('continuously reuse', async function() {
- const refs = [createRef(), createRef(), createRef()];
- const signs = [0, 0, 0];
-
- class ListItem extends Component {
- render() {
- return ;
- }
- }
+describe('ui operations', () => {
+ it('should delay until hydration finished', async function() {
+ const ref1 = vi.fn((ref) => {
+ ref.invoke({
+ method: 'boundingClientRect',
+ }).exec();
+ });
class Comp extends Component {
+ x = 'x';
render() {
return (
-
- {[0, 1, 2].map((index) => {
- return (
-
-
-
- );
- })}
-
+
+
+
);
}
}
@@ -1543,141 +1352,105 @@ describe('element ref in list', () => {
{
globalEnvManager.switchToBackground();
render(, __root);
- lynx.getNativeApp().callLepusMethod.mockClear();
+ expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(`[]`);
}
// hydrate
{
- // LifecycleConstant.firstScreen
lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
globalThis.__OnLifecycleEvent.mockClear();
-
- // rLynxChange
- globalEnvManager.switchToMainThread();
- const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
- globalThis[rLynxChange[0]](rLynxChange[1]);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`[]`);
- }
-
- // list render item 1
- {
- signs[0] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 0);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
+ expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(`
[
[
+ "[react-ref--2-0]",
+ "invoke",
[
- "rLynxRef",
{
- "commitTaskId": undefined,
- "refPatch": "{"-4:0:":58}",
+ "method": "boundingClientRect",
},
],
],
]
`);
+ lynx.createSelectorQuery().constructor.execLog.mockClear();
+ }
+ });
+
+ it('should support more usages of ref 1', async function() {
+ const ref1 = vi.fn((ref) => {
+ ref.setNativeProps({
+ 'background-color': 'blue',
+ }).exec();
+ ref.path(vi.fn()).exec();
+ });
+
+ class Comp extends Component {
+ x = 'x';
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ // main thread render
+ {
+ __root.__jsx = ;
+ renderPage();
+ }
+
+ // background render
+ {
globalEnvManager.switchToBackground();
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- globalThis.__OnLifecycleEvent.mockClear();
- expect(refs[0]).toMatchInlineSnapshot(`
- {
- "current": {
- "selectUniqueID": [Function],
- "uid": 58,
- },
- }
- `);
- expect(refs[1].current).toBeNull();
- expect(refs[2].current).toBeNull();
+ render(, __root);
}
- // list enqueue item 1 & render item 2
+ // hydrate
{
- globalEnvManager.switchToMainThread();
- elementTree.triggerEnqueueComponent(__root.childNodes[0].__elements[0], signs[0]);
- signs[1] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 1);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
+ lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
+ globalThis.__OnLifecycleEvent.mockClear();
+ expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(`
[
[
+ "[react-ref--2-0]",
+ "setNativeProps",
[
- "rLynxRef",
{
- "commitTaskId": undefined,
- "refPatch": "{"-4:0:":null,"-6:0:":58}",
+ "background-color": "blue",
},
],
],
- ]
- `);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- globalThis.__OnLifecycleEvent.mockClear();
- expect(refs[0].current).toBeNull();
- expect(refs[1]).toMatchInlineSnapshot(`
- {
- "current": {
- "selectUniqueID": [Function],
- "uid": 58,
- },
- }
- `);
- expect(refs[2].current).toBeNull();
- }
-
- // list enqueue item 2 & render item 3
- {
- globalEnvManager.switchToMainThread();
- elementTree.triggerEnqueueComponent(__root.childNodes[0].__elements[0], signs[1]);
- signs[2] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 2);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
- [
[
+ "[react-ref--2-0]",
+ "path",
[
- "rLynxRef",
- {
- "commitTaskId": undefined,
- "refPatch": "{"-6:0:":null,"-8:0:":58}",
- },
+ [MockFunction spy],
],
],
]
`);
- lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
- globalThis.__OnLifecycleEvent.mockClear();
- expect(refs[0].current).toBeNull();
- expect(refs[1].current).toBeNull();
- expect(refs[2]).toMatchInlineSnapshot(`
- {
- "current": {
- "selectUniqueID": [Function],
- "uid": 58,
- },
- }
- `);
+ lynx.createSelectorQuery().constructor.execLog.mockClear();
}
});
- it('when __FIRST_SCREEN_SYNC_TIMING__ is jsReady', async function() {
- globalThis.__FIRST_SCREEN_SYNC_TIMING__ = 'jsReady';
- const refs = [createRef(), createRef(), createRef()];
- const signs = [0, 0, 0];
-
- class ListItem extends Component {
- render() {
- return ;
- }
- }
+ it('should support more usages of ref 2', async function() {
+ const ref1 = vi.fn((ref) => {
+ const fields = ref.fields({
+ id: true,
+ });
+ fields.exec();
+ fields.exec();
+ });
class Comp extends Component {
+ x = 'x';
render() {
return (
-
- {[0, 1, 2].map((index) => {
- return (
-
-
-
- );
- })}
-
+
+
+
);
}
}
@@ -1688,71 +1461,159 @@ describe('element ref in list', () => {
renderPage();
}
- // list render item 1 & 2
+ // background render
{
- signs[0] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 0);
- expect(globalThis.__OnLifecycleEvent).toHaveBeenCalledTimes(1);
-
globalEnvManager.switchToBackground();
+ render(, __root);
+ }
+
+ // hydrate
+ {
lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
globalThis.__OnLifecycleEvent.mockClear();
- expect(delayedLifecycleEvents).toMatchInlineSnapshot(`
+ expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(`
[
[
- "rLynxRef",
- {
- "commitTaskId": undefined,
- "refPatch": "{"-4:0:":62}",
- },
+ "[react-ref--2-0]",
+ "fields",
+ [
+ {
+ "id": true,
+ },
+ ],
+ ],
+ [
+ "[react-ref--2-0]",
+ "fields",
+ [
+ {
+ "id": true,
+ },
+ ],
],
]
`);
+ lynx.createSelectorQuery().constructor.execLog.mockClear();
+ }
+ });
+
+ it('should not delay after hydration', async function() {
+ const ref1 = createRef();
+
+ function Comp() {
+ return (
+
+
+
+ );
+ }
+
+ // main thread render
+ {
+ __root.__jsx = ;
+ renderPage();
}
// background render
{
globalEnvManager.switchToBackground();
- root.render(, __root);
- expect(lynx.getNativeApp().callLepusMethod).toHaveBeenCalledTimes(1);
- expect(lynx.getNativeApp().callLepusMethod.mock.calls[0]).toMatchInlineSnapshot(`
- [
- "rLynxJSReady",
- {},
- ]
- `);
- globalEnvManager.switchToMainThread();
- const rLynxJSReady = lynx.getNativeApp().callLepusMethod.mock.calls[0];
- globalThis[rLynxJSReady[0]](rLynxJSReady[1]);
+ render(, __root);
+ }
+
+ // hydrate
+ {
+ lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
+ globalThis.__OnLifecycleEvent.mockClear();
+ expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(`[]`);
+
+ lynx.createSelectorQuery().constructor.execLog.mockClear();
lynx.getNativeApp().callLepusMethod.mockClear();
- expect(globalThis.__OnLifecycleEvent).toHaveBeenCalledTimes(1);
- expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
+ }
+
+ // call ref
+ {
+ ref1.current.invoke({
+ method: 'boundingClientRect',
+ }).exec();
+ expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(`
[
[
+ "[react-ref--2-0]",
+ "invoke",
[
- "rLynxFirstScreen",
{
- "jsReadyEventIdSwap": {},
- "refPatch": "{}",
- "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__Card__:__snapshot_a94a8_test_24","children":[{"id":-3,"type":"__Card__:__snapshot_a94a8_test_25","values":[{"item-key":0}],"children":[{"id":-4,"type":"__Card__:__snapshot_a94a8_test_23","values":["-4:0:"]}]},{"id":-5,"type":"__Card__:__snapshot_a94a8_test_25","values":[{"item-key":1}],"children":[{"id":-6,"type":"__Card__:__snapshot_a94a8_test_23","values":["-6:0:"]}]},{"id":-7,"type":"__Card__:__snapshot_a94a8_test_25","values":[{"item-key":2}],"children":[{"id":-8,"type":"__Card__:__snapshot_a94a8_test_23","values":["-8:0:"]}]}]}]}",
+ "method": "boundingClientRect",
},
],
],
]
`);
}
+ });
+
+ it('should not delay after hydration', async function() {
+ const ref1 = vi.fn((ref) => {
+ ref.invoke({
+ method: 'boundingClientRect',
+ }).exec();
+ });
+ let show = false;
+ function Child() {
+ return ;
+ }
+
+ function Comp() {
+ return (
+
+ {show ? : null}
+
+ );
+ }
+
+ // main thread render
+ {
+ __root.__jsx = ;
+ renderPage();
+ }
+
+ // background render
{
globalEnvManager.switchToBackground();
+ render(, __root);
+ }
+
+ // hydrate
+ {
lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
globalThis.__OnLifecycleEvent.mockClear();
+ expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(`[]`);
- expect(refs[0].current).toMatchInlineSnapshot(`
- {
- "selectUniqueID": [Function],
- "uid": 62,
- }
+ lynx.createSelectorQuery().constructor.execLog.mockClear();
+ lynx.getNativeApp().callLepusMethod.mockClear();
+ }
+
+ // set show
+ {
+ show = true;
+ render(, __root);
+
+ expect(lynx.getNativeApp().callLepusMethod.mock.calls[0][1].data).toMatchInlineSnapshot(
+ `"{"patchList":[{"id":3,"snapshotPatch":[0,"__Card__:__snapshot_a94a8_test_26",3,4,3,[1],1,-2,3,null]}]}"`,
+ );
+ expect(lynx.createSelectorQuery().constructor.execLog.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ "[react-ref-3-0]",
+ "invoke",
+ [
+ {
+ "method": "boundingClientRect",
+ },
+ ],
+ ],
+ ]
`);
}
- globalThis.__FIRST_SCREEN_SYNC_TIMING__ = 'immediately';
});
});
diff --git a/packages/react/runtime/__test__/ssr.test.jsx b/packages/react/runtime/__test__/ssr.test.jsx
index 589ba928c7..3a3920d0d0 100644
--- a/packages/react/runtime/__test__/ssr.test.jsx
+++ b/packages/react/runtime/__test__/ssr.test.jsx
@@ -1,12 +1,13 @@
/** @jsxImportSource ../lepus */
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
-import { elementTree, options } from './utils/nativeMethod';
+
import { globalEnvManager } from './utils/envManager';
-import { __root } from '../src/root';
+import { elementTree, options } from './utils/nativeMethod';
import { __page as __internalPage } from '../src/internal';
-import { clearPage } from '../src/snapshot';
import { jsReadyEventIdSwap } from '../src/lifecycle/event/jsReady';
+import { __root } from '../src/root';
+import { clearPage } from '../src/snapshot';
const ssrIDMap = new Map();
@@ -211,7 +212,7 @@ describe('ssr', () => {
"color": "red",
},
"-2:2:",
- "-2:3:",
+ "react-ref--2-3",
{
"_lepusWorkletHash": "1",
"_workletType": "main-thread",
@@ -273,7 +274,7 @@ describe('ssr', () => {
"main-thread:ref": {
"_lepusWorkletHash": "2",
},
- "ref": "-2:0:ref",
+ "ref": "react-ref--2-0",
"style": {
"color": "red",
},
diff --git a/packages/react/runtime/__test__/utils/envManager.ts b/packages/react/runtime/__test__/utils/envManager.ts
index 21e6d6b261..bb73467090 100644
--- a/packages/react/runtime/__test__/utils/envManager.ts
+++ b/packages/react/runtime/__test__/utils/envManager.ts
@@ -3,13 +3,15 @@
// 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 { setupBackgroundDocument, setupDocument } from '../../src/document.js';
-import { __root, setRoot } from '../../src/root.js';
import { BackgroundSnapshotInstance } from '../../src/backgroundSnapshot.js';
-import { backgroundSnapshotInstanceManager, SnapshotInstance, snapshotInstanceManager } from '../../src/snapshot.js';
+import { setupBackgroundDocument, setupDocument } from '../../src/document.js';
import { deinitGlobalSnapshotPatch } from '../../src/lifecycle/patch/snapshotPatch.js';
-import { globalPipelineOptions, setPipeline } from '../../src/lynx/performance.js';
+import { shouldDelayUiOps } from '../../src/lifecycle/ref/delay.js';
import { clearListGlobal } from '../../src/list.js';
+import { globalPipelineOptions, setPipeline } from '../../src/lynx/performance.js';
+import { __root, setRoot } from '../../src/root.js';
+import { SnapshotInstance, backgroundSnapshotInstanceManager, snapshotInstanceManager } from '../../src/snapshot.js';
+import { hydrationMap } from '../../src/snapshotInstanceHydrationMap.js';
export class EnvManager {
root: typeof __root | undefined;
@@ -70,6 +72,8 @@ export class EnvManager {
backgroundSnapshotInstanceManager.nextId = 0;
snapshotInstanceManager.clear();
snapshotInstanceManager.nextId = 0;
+ hydrationMap.clear();
+ shouldDelayUiOps.value = true;
clearListGlobal();
deinitGlobalSnapshotPatch();
this.switchToBackground();
diff --git a/packages/react/runtime/__test__/utils/globals.js b/packages/react/runtime/__test__/utils/globals.js
index d2928a9df7..c6604cbb3f 100644
--- a/packages/react/runtime/__test__/utils/globals.js
+++ b/packages/react/runtime/__test__/utils/globals.js
@@ -35,6 +35,46 @@ const performance = {
}),
};
+class SelectorQuery {
+ static execLog = vi.fn();
+ id = '';
+ method = '';
+ params = undefined;
+
+ select(id) {
+ this.id = id;
+ return this;
+ }
+
+ invoke(...args) {
+ this.method = 'invoke';
+ this.params = args;
+ return this;
+ }
+
+ path(...args) {
+ this.method = 'path';
+ this.params = args;
+ return this;
+ }
+
+ fields(...args) {
+ this.method = 'fields';
+ this.params = args;
+ return this;
+ }
+
+ setNativeProps(...args) {
+ this.method = 'setNativeProps';
+ this.params = args;
+ return this;
+ }
+
+ exec() {
+ SelectorQuery.execLog(this.id, this.method, this.params);
+ }
+}
+
function injectGlobals() {
globalThis.__DEV__ = true;
globalThis.__PROFILE__ = true;
@@ -52,11 +92,28 @@ function injectGlobals() {
globalThis.lynx = {
getNativeApp: () => app,
performance,
- createSelectorQuery: vi.fn(() => {
+ createSelectorQuery: () => {
+ return new SelectorQuery();
+ },
+ getElementByIdTasks: vi.fn(),
+ getElementById: vi.fn((id) => {
return {
- selectUniqueID: function(uid) {
- this.uid = uid;
- return this;
+ animate: vi.fn(() => {
+ lynx.getElementByIdTasks('animate');
+ return {
+ play: () => {
+ lynx.getElementByIdTasks('play');
+ },
+ pause: () => {
+ lynx.getElementByIdTasks('pause');
+ },
+ cancel: () => {
+ lynx.getElementByIdTasks('cancel');
+ },
+ };
+ }),
+ setProperty: (property, value) => {
+ lynx.getElementByIdTasks('setProperty', property, value);
},
};
}),
diff --git a/packages/react/runtime/lazy/internal.js b/packages/react/runtime/lazy/internal.js
index f26c184d94..bac01f2d06 100644
--- a/packages/react/runtime/lazy/internal.js
+++ b/packages/react/runtime/lazy/internal.js
@@ -16,6 +16,7 @@ export const {
__page,
__pageId,
__root,
+ applyRefs,
createSnapshot,
loadDynamicJS,
loadLazyBundle,
diff --git a/packages/react/runtime/src/backgroundSnapshot.ts b/packages/react/runtime/src/backgroundSnapshot.ts
index cc27f82b48..02a1f8452f 100644
--- a/packages/react/runtime/src/backgroundSnapshot.ts
+++ b/packages/react/runtime/src/backgroundSnapshot.ts
@@ -22,6 +22,7 @@ import {
takeGlobalSnapshotPatch,
} from './lifecycle/patch/snapshotPatch.js';
import { globalPipelineOptions } from './lynx/performance.js';
+import { transformSpread } from './snapshot/spread.js';
import type { SerializedSnapshotInstance } from './snapshot.js';
import {
DynamicPartType,
@@ -29,8 +30,7 @@ import {
snapshotManager,
traverseSnapshotInstance,
} from './snapshot.js';
-import { markRefToRemove } from './snapshot/ref.js';
-import { transformSpread } from './snapshot/spread.js';
+import { hydrationMap } from './snapshotInstanceHydrationMap.js';
import { isDirectOrDeepEqual } from './utils.js';
import { onPostWorkletCtx } from './worklet/ctx.js';
@@ -188,7 +188,7 @@ export class BackgroundSnapshotInstance {
for (let index = 0; index < value.length; index++) {
const { needUpdate, valueToCommit } = this.setAttributeImpl(value[index], oldValues[index], index);
if (needUpdate) {
- __globalSnapshotPatch?.push(
+ __globalSnapshotPatch!.push(
SnapshotOperation.SetAttribute,
this.__id,
index,
@@ -203,7 +203,7 @@ export class BackgroundSnapshotInstance {
const { valueToCommit } = this.setAttributeImpl(value[index], null, index);
patch[index] = valueToCommit;
}
- __globalSnapshotPatch?.push(
+ __globalSnapshotPatch!.push(
SnapshotOperation.SetAttributes,
this.__id,
patch,
@@ -238,9 +238,6 @@ export class BackgroundSnapshotInstance {
valueToCommit: any;
} {
if (!newValue) {
- if (oldValue && oldValue.__ref) {
- markRefToRemove(`${this.__id}:${index}:`, oldValue);
- }
return { needUpdate: oldValue !== newValue, valueToCommit: newValue };
}
@@ -253,10 +250,7 @@ export class BackgroundSnapshotInstance {
// use __spread to cache the transform result for next diff
newValue.__spread = newSpread;
if (needUpdate) {
- if (oldSpread && oldSpread.ref) {
- markRefToRemove(`${this.__id}:${index}:ref`, oldValue.ref);
- }
- for (let key in newSpread) {
+ for (const key in newSpread) {
const newSpreadValue = newSpread[key];
if (!newSpreadValue) {
continue;
@@ -265,22 +259,15 @@ export class BackgroundSnapshotInstance {
newSpread[key] = onPostWorkletCtx(newSpreadValue as Worklet);
} else if ((newSpreadValue as any).__isGesture) {
processGestureBackground(newSpreadValue as GestureKind);
- } else if (key == '__lynx_timing_flag' && oldSpread?.[key] != newSpreadValue) {
- if (globalPipelineOptions) {
- globalPipelineOptions.needTimestamps = true;
- }
+ } else if (key == '__lynx_timing_flag' && oldSpread?.[key] != newSpreadValue && globalPipelineOptions) {
+ globalPipelineOptions.needTimestamps = true;
}
}
}
return { needUpdate, valueToCommit: newSpread };
}
if (newValue.__ref) {
- // force update to update ref value
- // TODO: ref: optimize this. The ref update maybe can be done on the background thread to reduce updating.
- // The old ref must have a place to be stored because it needs to be cleared when the main thread returns.
- markRefToRemove(`${this.__id}:${index}:`, oldValue);
- // update ref. On the main thread, the ref id will be replaced with value's sign when updating.
- return { needUpdate: true, valueToCommit: newValue.__ref };
+ return { needUpdate: false, valueToCommit: 1 };
}
if (newValue._wkltId) {
return { needUpdate: true, valueToCommit: onPostWorkletCtx(newValue) };
@@ -301,8 +288,7 @@ export class BackgroundSnapshotInstance {
}
if (newType === 'function') {
if (newValue.__ref) {
- markRefToRemove(`${this.__id}:${index}:`, oldValue);
- return { needUpdate: true, valueToCommit: newValue.__ref };
+ return { needUpdate: false, valueToCommit: 1 };
}
/* event */
return { needUpdate: !oldValue, valueToCommit: 1 };
@@ -335,6 +321,7 @@ export function hydrate(
before: SerializedSnapshotInstance,
after: BackgroundSnapshotInstance,
) => {
+ hydrationMap.set(after.__id, before.id);
backgroundSnapshotInstanceManager.updateId(after.__id, before.id);
after.__values?.forEach((value, index) => {
const old = before.values![index];
@@ -344,7 +331,7 @@ export function hydrate(
// `value.__spread` my contain event ids using snapshot ids before hydration. Remove it.
delete value.__spread;
value = transformSpread(after, index, value);
- for (let key in value) {
+ for (const key in value) {
if (value[key] && value[key]._wkltId) {
onPostWorkletCtx(value[key]);
} else if (value[key] && value[key].__isGesture) {
@@ -353,12 +340,8 @@ export function hydrate(
}
after.__values![index]!.__spread = value;
} else if (value.__ref) {
- if (old) {
- // skip patch
- value = old;
- } else {
- value = value.__ref;
- }
+ // skip patch
+ value = old;
} else if (typeof value === 'function') {
value = `${after.__id}:${index}:`;
}
diff --git a/packages/react/runtime/src/hooks/react.ts b/packages/react/runtime/src/hooks/react.ts
index c582e96e40..95d4665884 100644
--- a/packages/react/runtime/src/hooks/react.ts
+++ b/packages/react/runtime/src/hooks/react.ts
@@ -9,7 +9,7 @@ import {
useId,
useImperativeHandle,
useMemo,
- useLayoutEffect as usePreactLayoutEffect,
+ useEffect as usePreactEffect,
useReducer,
useRef,
useState,
@@ -29,7 +29,7 @@ import type { DependencyList, EffectCallback } from 'react';
* @deprecated `useLayoutEffect` in the background thread cannot offer the precise timing for reading layout information and synchronously re-render, which is different from React.
*/
function useLayoutEffect(effect: EffectCallback, deps?: DependencyList): void {
- return usePreactLayoutEffect(effect, deps);
+ return usePreactEffect(effect, deps);
}
/**
@@ -42,7 +42,7 @@ function useLayoutEffect(effect: EffectCallback, deps?: DependencyList): void {
* @public
*/
function useEffect(effect: EffectCallback, deps?: DependencyList): void {
- return usePreactLayoutEffect(effect, deps);
+ return usePreactEffect(effect, deps);
}
export {
diff --git a/packages/react/runtime/src/internal.ts b/packages/react/runtime/src/internal.ts
index 40c8063496..d66f23754b 100644
--- a/packages/react/runtime/src/internal.ts
+++ b/packages/react/runtime/src/internal.ts
@@ -26,7 +26,7 @@ export const __DynamicPartChildren_0: [DynamicPartType, number][] = [[DynamicPar
export { updateSpread } from './snapshot/spread.js';
export { updateEvent } from './snapshot/event.js';
-export { updateRef, transformRef } from './snapshot/ref.js';
+export { updateRef, transformRef, applyRefs } from './snapshot/ref.js';
export { updateWorkletEvent } from './snapshot/workletEvent.js';
export { updateWorkletRef } from './snapshot/workletRef.js';
export { updateGesture } from './snapshot/gesture.js';
diff --git a/packages/react/runtime/src/lifecycle/event/delayLifecycleEvents.ts b/packages/react/runtime/src/lifecycle/event/delayLifecycleEvents.ts
index 1b0c61a6da..4df1b7bb80 100644
--- a/packages/react/runtime/src/lifecycle/event/delayLifecycleEvents.ts
+++ b/packages/react/runtime/src/lifecycle/event/delayLifecycleEvents.ts
@@ -1,18 +1,7 @@
-import { LifecycleConstant } from '../../lifecycleConstant.js';
-
const delayedLifecycleEvents: [type: string, data: any][] = [];
function delayLifecycleEvent(type: string, data: any): void {
- // We need to ensure that firstScreen events are executed before other events.
- // This is because firstScreen events are used to initialize the dom tree,
- // and other events depend on the dom tree being fully constructed.
- // There might be some edge cases where ctx cannot be found in `ref` lifecycle event,
- // and they should be ignored safely.
- if (type === LifecycleConstant.firstScreen) {
- delayedLifecycleEvents.unshift([type, data]);
- } else {
- delayedLifecycleEvents.push([type, data]);
- }
+ delayedLifecycleEvents.push([type, data]);
}
/**
diff --git a/packages/react/runtime/src/lifecycle/event/jsReady.ts b/packages/react/runtime/src/lifecycle/event/jsReady.ts
index eaa1d23765..93863d5e91 100644
--- a/packages/react/runtime/src/lifecycle/event/jsReady.ts
+++ b/packages/react/runtime/src/lifecycle/event/jsReady.ts
@@ -1,6 +1,5 @@
import { LifecycleConstant } from '../../lifecycleConstant.js';
import { __root } from '../../root.js';
-import { takeGlobalRefPatchMap } from '../../snapshot/ref.js';
let isJSReady: boolean;
let jsReadyEventIdSwap: Record;
@@ -11,7 +10,6 @@ function jsReady(): void {
LifecycleConstant.firstScreen, /* FIRST_SCREEN */
{
root: JSON.stringify(__root),
- refPatch: JSON.stringify(takeGlobalRefPatchMap()),
jsReadyEventIdSwap,
},
]);
diff --git a/packages/react/runtime/src/lifecycle/patch/commit.ts b/packages/react/runtime/src/lifecycle/patch/commit.ts
index ad3937e3f2..57d1b4cee2 100644
--- a/packages/react/runtime/src/lifecycle/patch/commit.ts
+++ b/packages/react/runtime/src/lifecycle/patch/commit.ts
@@ -32,8 +32,8 @@ import {
setPipeline,
} from '../../lynx/performance.js';
import { CATCH_ERROR, COMMIT, RENDER_CALLBACKS, VNODE } from '../../renderToOpcodes/constants.js';
+import { applyDelayedRefs } from '../../snapshot/ref.js';
import { backgroundSnapshotInstanceManager } from '../../snapshot.js';
-import { updateBackgroundRefs } from '../../snapshot/ref.js';
import { isEmptyObject } from '../../utils.js';
import { takeWorkletRefInitValuePatch } from '../../worklet/workletRefPool.js';
import { runDelayedUnmounts, takeDelayedUnmounts } from '../delayUnmount.js';
@@ -43,7 +43,7 @@ import { takeGlobalSnapshotPatch } from './snapshotPatch.js';
let globalFlushOptions: FlushOptions = {};
-const globalCommitTaskMap: Map void> = /*@__PURE__*/ new Map();
+const globalCommitTaskMap: Map void> = /*@__PURE__*/ new Map void>();
let nextCommitTaskId = 1;
let globalBackgroundSnapshotInstancesToRemove: number[] = [];
@@ -52,6 +52,7 @@ let globalBackgroundSnapshotInstancesToRemove: number[] = [];
* A single patch operation.
*/
interface Patch {
+ // TODO: ref: do we need `id`?
id: number;
snapshotPatch?: SnapshotPatch;
workletRefInitValuePatch?: [id: number, value: unknown][];
@@ -112,7 +113,6 @@ function replaceCommitHook(): void {
// Register the commit task
globalCommitTaskMap.set(commitTaskId, () => {
- updateBackgroundRefs(commitTaskId);
runDelayedUnmounts(delayedUnmounts);
originalPreactCommit?.(vnode, renderCallbacks);
renderCallbacks.some(wrapper => {
@@ -140,6 +140,7 @@ function replaceCommitHook(): void {
globalFlushOptions = {};
if (!snapshotPatch && workletRefInitValuePatch.length === 0) {
// before hydration, skip patch
+ applyDelayedRefs();
return;
}
@@ -169,6 +170,8 @@ function replaceCommitHook(): void {
globalCommitTaskMap.delete(commitTaskId);
}
});
+
+ applyDelayedRefs();
};
options[COMMIT] = commit as ((...args: Parameters) => void);
}
@@ -243,7 +246,6 @@ export {
globalBackgroundSnapshotInstancesToRemove,
globalCommitTaskMap,
globalFlushOptions,
- nextCommitTaskId,
replaceCommitHook,
replaceRequestAnimationFrame,
type PatchList,
diff --git a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts
index da2001ba59..bf261a9884 100644
--- a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts
+++ b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts
@@ -4,15 +4,13 @@
import { clearDelayedWorklets, updateWorkletRefInitValueChanges } from '@lynx-js/react/worklet-runtime/bindings';
-import type { PatchList, PatchOptions } from './commit.js';
-import { snapshotPatchApply } from './snapshotPatchApply.js';
import { LifecycleConstant } from '../../lifecycleConstant.js';
import { __pendingListUpdates } from '../../list.js';
import { PerformanceTimingKeys, markTiming, setPipeline } from '../../lynx/performance.js';
-import { takeGlobalRefPatchMap } from '../../snapshot/ref.js';
import { __page } from '../../snapshot.js';
-import { isEmptyObject } from '../../utils.js';
import { getReloadVersion } from '../pass.js';
+import type { PatchList, PatchOptions } from './commit.js';
+import { snapshotPatchApply } from './snapshotPatchApply.js';
function updateMainThread(
{ data, patchOptions }: {
@@ -32,7 +30,7 @@ function updateMainThread(
markTiming(PerformanceTimingKeys.parseChangesEnd);
markTiming(PerformanceTimingKeys.patchChangesStart);
- for (const { snapshotPatch, workletRefInitValuePatch, id } of patchList) {
+ for (const { snapshotPatch, workletRefInitValuePatch } of patchList) {
updateWorkletRefInitValueChanges(workletRefInitValuePatch);
__pendingListUpdates.clear();
if (snapshotPatch) {
@@ -41,8 +39,6 @@ function updateMainThread(
__pendingListUpdates.flush();
// console.debug('********** Lepus updatePatch:');
// printSnapshotInstance(snapshotInstanceManager.values.get(-1)!);
-
- commitMainThreadPatchUpdate(id);
}
markTiming(PerformanceTimingKeys.patchChangesEnd);
markTiming(PerformanceTimingKeys.mtsRenderEnd);
@@ -60,14 +56,7 @@ function injectUpdateMainThread(): void {
Object.assign(globalThis, { [LifecycleConstant.patchUpdate]: updateMainThread });
}
-function commitMainThreadPatchUpdate(commitTaskId?: number): void {
- const refPatch = takeGlobalRefPatchMap();
- if (!isEmptyObject(refPatch)) {
- __OnLifecycleEvent([LifecycleConstant.ref, { commitTaskId, refPatch: JSON.stringify(refPatch) }]);
- }
-}
-
/**
* @internal
*/
-export { commitMainThreadPatchUpdate, injectUpdateMainThread };
+export { injectUpdateMainThread };
diff --git a/packages/react/runtime/src/lifecycle/ref/delay.ts b/packages/react/runtime/src/lifecycle/ref/delay.ts
new file mode 100644
index 0000000000..8cea860c47
--- /dev/null
+++ b/packages/react/runtime/src/lifecycle/ref/delay.ts
@@ -0,0 +1,99 @@
+// Copyright 2024 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 type { NodesRef, SelectorQuery } from '@lynx-js/types';
+
+import { hydrationMap } from '../../snapshotInstanceHydrationMap.js';
+
+type RefTask = (nodesRef: NodesRef) => SelectorQuery;
+
+/**
+ * A flag to indicate whether UI operations should be delayed.
+ * When set to true, UI operations will be queued in the `delayedUiOps` array
+ * and executed later when `runDelayedUiOps` is called.
+ * This is used before hydration to ensure UI operations are batched
+ * and executed at the appropriate time.
+ */
+const shouldDelayUiOps = { value: true };
+
+/**
+ * An array of functions that will be executed later when `runDelayedUiOps` is called.
+ * These functions contain UI operations that need to be delayed.
+ */
+const delayedUiOps: (() => void)[] = [];
+
+/**
+ * Runs a task either immediately or delays it based on the `shouldDelayUiOps` flag.
+ * @param task - The function to execute.
+ */
+function runOrDelay(task: () => void): void {
+ if (shouldDelayUiOps.value) {
+ delayedUiOps.push(task);
+ } else {
+ task();
+ }
+}
+
+/**
+ * Executes all delayed UI operations.
+ */
+function runDelayedUiOps(): void {
+ for (const task of delayedUiOps) {
+ task();
+ }
+ shouldDelayUiOps.value = false;
+ delayedUiOps.length = 0;
+}
+
+/**
+ * A proxy class designed for managing and executing reference-based tasks.
+ * It delays the execution of tasks until hydration is complete.
+ */
+class RefProxy {
+ private readonly refAttr: [snapshotInstanceId: number, expIndex: number];
+ private task: RefTask | undefined;
+
+ constructor(refAttr: [snapshotInstanceId: number, expIndex: number]) {
+ this.refAttr = refAttr;
+ }
+
+ private setTask(
+ method: K,
+ args: Parameters,
+ ): this {
+ this.task = (nodesRef) => {
+ return (nodesRef[method] as unknown as (...args: any[]) => SelectorQuery)(...args);
+ };
+ return this;
+ }
+
+ invoke(...args: Parameters): RefProxy {
+ return new RefProxy(this.refAttr).setTask('invoke', args);
+ }
+
+ path(...args: Parameters): RefProxy {
+ return new RefProxy(this.refAttr).setTask('path', args);
+ }
+
+ fields(...args: Parameters): RefProxy {
+ return new RefProxy(this.refAttr).setTask('fields', args);
+ }
+
+ setNativeProps(...args: Parameters): RefProxy {
+ return new RefProxy(this.refAttr).setTask('setNativeProps', args);
+ }
+
+ exec(): void {
+ runOrDelay(() => {
+ const realRefId = hydrationMap.get(this.refAttr[0]) ?? this.refAttr[0];
+ const refSelector = `[react-ref-${realRefId}-${this.refAttr[1]}]`;
+ this.task!(lynx.createSelectorQuery().select(refSelector)).exec();
+ });
+ }
+}
+
+/**
+ * @internal
+ */
+export { RefProxy, runDelayedUiOps, shouldDelayUiOps };
diff --git a/packages/react/runtime/src/lifecycle/reload.ts b/packages/react/runtime/src/lifecycle/reload.ts
index 7ffd950e0b..32bf5cf29d 100644
--- a/packages/react/runtime/src/lifecycle/reload.ts
+++ b/packages/react/runtime/src/lifecycle/reload.ts
@@ -14,13 +14,13 @@ import { LifecycleConstant } from '../lifecycleConstant.js';
import { __pendingListUpdates } from '../list.js';
import { __root, setRoot } from '../root.js';
import { SnapshotInstance, __page, snapshotInstanceManager } from '../snapshot.js';
-import { takeGlobalRefPatchMap } from '../snapshot/ref.js';
import { isEmptyObject } from '../utils.js';
-import { destroyWorklet } from '../worklet/destroy.js';
import { destroyBackground } from './destroy.js';
+import { destroyWorklet } from '../worklet/destroy.js';
import { clearJSReadyEventIdSwap, isJSReady } from './event/jsReady.js';
import { increaseReloadVersion } from './pass.js';
import { deinitGlobalSnapshotPatch } from './patch/snapshotPatch.js';
+import { shouldDelayUiOps } from './ref/delay.js';
import { renderMainThread } from './render.js';
function reloadMainThread(data: any, options: UpdatePageOption): void {
@@ -55,7 +55,6 @@ function reloadMainThread(data: any, options: UpdatePageOption): void {
LifecycleConstant.firstScreen, /* FIRST_SCREEN */
{
root: JSON.stringify(__root),
- refPatch: JSON.stringify(takeGlobalRefPatchMap()),
},
]);
}
@@ -82,6 +81,7 @@ function reloadBackground(updateData: Record): void {
// COW when modify `lynx.__initData` to make sure Provider & Consumer works
lynx.__initData = Object.assign({}, lynx.__initData, updateData);
+ shouldDelayUiOps.value = true;
render(__root.__jsx, __root as any);
if (__PROFILE__) {
diff --git a/packages/react/runtime/src/lifecycleConstant.ts b/packages/react/runtime/src/lifecycleConstant.ts
index aaf25f033d..de5d956ce1 100644
--- a/packages/react/runtime/src/lifecycleConstant.ts
+++ b/packages/react/runtime/src/lifecycleConstant.ts
@@ -5,7 +5,6 @@ export class LifecycleConstant {
public static readonly firstScreen = 'rLynxFirstScreen';
public static readonly updateFromRoot = 'updateFromRoot';
public static readonly globalEventFromLepus = 'globalEventFromLepus';
- public static readonly ref = 'rLynxRef';
public static readonly jsReady = 'rLynxJSReady';
public static readonly patchUpdate = 'rLynxChange';
}
diff --git a/packages/react/runtime/src/list.ts b/packages/react/runtime/src/list.ts
index 9049146d75..973f1469bc 100644
--- a/packages/react/runtime/src/list.ts
+++ b/packages/react/runtime/src/list.ts
@@ -2,7 +2,6 @@
// 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 { hydrate } from './hydrate.js';
-import { commitMainThreadPatchUpdate } from './lifecycle/patch/updateMainThread.js';
import type { SnapshotInstance } from './snapshot.js';
export interface ListUpdateInfo {
@@ -339,7 +338,6 @@ export function componentAtIndexFactory(
__FlushElementTree(root, flushOptions);
}
signMap.set(sign, childCtx);
- commitMainThreadPatchUpdate(undefined);
return sign;
}
@@ -360,7 +358,6 @@ export function componentAtIndexFactory(
});
}
signMap.set(sign, childCtx);
- commitMainThreadPatchUpdate(undefined);
return sign;
};
diff --git a/packages/react/runtime/src/lynx/calledByNative.ts b/packages/react/runtime/src/lynx/calledByNative.ts
index ecf29588a0..46a451e8a8 100644
--- a/packages/react/runtime/src/lynx/calledByNative.ts
+++ b/packages/react/runtime/src/lynx/calledByNative.ts
@@ -9,7 +9,6 @@ import { LifecycleConstant } from '../lifecycleConstant.js';
import { __pendingListUpdates } from '../list.js';
import { ssrHydrateByOpcodes } from '../opcodes.js';
import { __root, setRoot } from '../root.js';
-import { takeGlobalRefPatchMap } from '../snapshot/ref.js';
import { SnapshotInstance, __page, setupPage } from '../snapshot.js';
import { isEmptyObject } from '../utils.js';
import { PerformanceTimingKeys, markTiming, setPipeline } from './performance.js';
@@ -118,9 +117,6 @@ function updatePage(data: any, options?: UpdatePageOption): void {
markTiming(PerformanceTimingKeys.updateDiffVdomStart);
{
__pendingListUpdates.clear();
-
- // ignore ref & unref before jsReady
- takeGlobalRefPatchMap();
renderMainThread();
// As said by codename `jsReadyEventIdSwap`, this swap will only be used for event remap,
// because ref & unref cause by previous render will be ignored
diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts
index 22679d9302..dc8a2a2f97 100644
--- a/packages/react/runtime/src/lynx/tt.ts
+++ b/packages/react/runtime/src/lynx/tt.ts
@@ -18,10 +18,10 @@ import { delayedEvents, delayedPublishEvent } from '../lifecycle/event/delayEven
import { delayLifecycleEvent, delayedLifecycleEvents } from '../lifecycle/event/delayLifecycleEvents.js';
import { commitPatchUpdate, genCommitTaskId, globalCommitTaskMap } from '../lifecycle/patch/commit.js';
import type { PatchList } from '../lifecycle/patch/commit.js';
+import { runDelayedUiOps } from '../lifecycle/ref/delay.js';
import { reloadBackground } from '../lifecycle/reload.js';
import { CHILDREN } from '../renderToOpcodes/constants.js';
import { __root } from '../root.js';
-import { globalRefsToSet, updateBackgroundRefs } from '../snapshot/ref.js';
import { backgroundSnapshotInstanceManager } from '../snapshot.js';
import { destroyWorklet } from '../worklet/destroy.js';
@@ -72,7 +72,7 @@ function onLifecycleEvent([type, data]: [string, any]) {
function onLifecycleEventImpl(type: string, data: any): void {
switch (type) {
case LifecycleConstant.firstScreen: {
- const { root: lepusSide, refPatch, jsReadyEventIdSwap } = data;
+ const { root: lepusSide, jsReadyEventIdSwap } = data;
if (__PROFILE__) {
console.profile('hydrate');
}
@@ -109,16 +109,6 @@ function onLifecycleEventImpl(type: string, data: any): void {
lynxCoreInject.tt.publishEvent = publishEvent;
lynxCoreInject.tt.publicComponentEvent = publicComponentEvent;
- if (__PROFILE__) {
- console.profile('patchRef');
- }
- if (refPatch) {
- globalRefsToSet.set(0, JSON.parse(refPatch));
- updateBackgroundRefs(0);
- }
- if (__PROFILE__) {
- console.profileEnd();
- }
// console.debug("********** After hydration:");
// printSnapshotInstance(__root as BackgroundSnapshotInstance);
if (__PROFILE__) {
@@ -131,7 +121,6 @@ function onLifecycleEventImpl(type: string, data: any): void {
const obj = commitPatchUpdate(patchList, { isHydration: true });
lynx.getNativeApp().callLepusMethod(LifecycleConstant.patchUpdate, obj, () => {
- updateBackgroundRefs(commitTaskId);
globalCommitTaskMap.forEach((commitTask, id) => {
if (id > commitTaskId) {
return;
@@ -140,6 +129,7 @@ function onLifecycleEventImpl(type: string, data: any): void {
globalCommitTaskMap.delete(id);
});
});
+ runDelayedUiOps();
break;
}
case LifecycleConstant.globalEventFromLepus: {
@@ -147,16 +137,6 @@ function onLifecycleEventImpl(type: string, data: any): void {
lynx.getJSModule('GlobalEventEmitter').trigger(eventName, params);
break;
}
- case LifecycleConstant.ref: {
- const { refPatch, commitTaskId } = data;
- if (commitTaskId) {
- globalRefsToSet.set(commitTaskId, JSON.parse(refPatch));
- } else {
- globalRefsToSet.set(0, JSON.parse(refPatch));
- updateBackgroundRefs(0);
- }
- break;
- }
}
}
diff --git a/packages/react/runtime/src/snapshot.ts b/packages/react/runtime/src/snapshot.ts
index 30ece552d1..846eef8b4d 100644
--- a/packages/react/runtime/src/snapshot.ts
+++ b/packages/react/runtime/src/snapshot.ts
@@ -166,7 +166,7 @@ export const backgroundSnapshotInstanceManager: {
if (!res || (res.length != 2 && res.length != 3)) {
throw new Error('Invalid ctx format: ' + str);
}
- let id = Number(res[0]);
+ const id = Number(res[0]);
const expIndex = Number(res[1]);
const ctx = this.values.get(id);
if (!ctx) {
@@ -266,7 +266,6 @@ export class SnapshotInstance {
__element_root?: FiberElement | undefined;
__values?: any[] | undefined;
__current_slot_index = 0;
- __ref_set?: Set;
__worklet_ref_set?: Set | Worklet>;
__listItemPlatformInfo?: any;
@@ -289,15 +288,15 @@ export class SnapshotInstance {
// CSS Scope is removed(We only need to call `__SetCSSId` when there is `entryName`)
// Or an old bundle(`__SetCSSId` is called in `create`), we skip calling `__SetCSSId`
if (entryName !== DEFAULT_ENTRY_NAME && entryName !== undefined) {
- __SetCSSId(this.__elements!, DEFAULT_CSS_ID, entryName);
+ __SetCSSId(this.__elements, DEFAULT_CSS_ID, entryName);
}
} else {
// cssId !== undefined
if (entryName !== DEFAULT_ENTRY_NAME && entryName !== undefined) {
// For lazy bundle, we need add `entryName` to the third params
- __SetCSSId(this.__elements!, cssId, entryName);
+ __SetCSSId(this.__elements, cssId, entryName);
} else {
- __SetCSSId(this.__elements!, cssId);
+ __SetCSSId(this.__elements, cssId);
}
}
@@ -554,7 +553,6 @@ export class SnapshotInstance {
return;
}
- // TODO: ref: can this be done on the background thread?
unref(child, true);
r();
if (this.__elements) {
diff --git a/packages/react/runtime/src/snapshot/ref.ts b/packages/react/runtime/src/snapshot/ref.ts
index 248be086af..26b31230f4 100644
--- a/packages/react/runtime/src/snapshot/ref.ts
+++ b/packages/react/runtime/src/snapshot/ref.ts
@@ -3,21 +3,18 @@
// LICENSE file in the root directory of this source tree.
import type { Worklet, WorkletRef } from '@lynx-js/react/worklet-runtime/bindings';
-import { nextCommitTaskId } from '../lifecycle/patch/commit.js';
-import { SnapshotInstance, backgroundSnapshotInstanceManager } from '../snapshot.js';
+import type { SnapshotInstance } from '../snapshot.js';
import { workletUnRef } from './workletRef.js';
+import type { BackgroundSnapshotInstance } from '../backgroundSnapshot.js';
+import { RefProxy } from '../lifecycle/ref/delay.js';
-let globalRefPatch: Record = {};
-const globalRefsToRemove: Map* commitId */ number, Map* sign */ string, /* ref */ any>> = /* @__PURE__ */ new Map();
-const globalRefsToSet: Map* commitId */ number, Record> = /* @__PURE__ */ new Map();
-let nextRefId = 1;
+const refsToApply: (Ref[] | BackgroundSnapshotInstance)[] = [];
-function unref(snapshot: SnapshotInstance, recursive: boolean): void {
- snapshot.__ref_set?.forEach(v => {
- globalRefPatch[v] = null;
- });
- snapshot.__ref_set?.clear();
+type Ref = (((ref: RefProxy) => () => void) | { current: RefProxy | null }) & {
+ _unmount?: () => void;
+};
+function unref(snapshot: SnapshotInstance, recursive: boolean): void {
snapshot.__worklet_ref_set?.forEach(v => {
if (v) {
workletUnRef(v as Worklet | WorkletRef);
@@ -32,91 +29,60 @@ function unref(snapshot: SnapshotInstance, recursive: boolean): void {
}
}
-function applyRef(ref: any, value: any) {
- // TODO: ref: exceptions thrown in user functions should be able to be caught by an Error Boundary
- if (typeof ref == 'function') {
- const hasRefUnmount = typeof ref._unmount == 'function';
- if (hasRefUnmount) {
- // @ts-ignore TS doesn't like moving narrowing checks into variables
- ref._unmount();
- }
+// This function is modified from preact source code.
+function applyRef(ref: Ref, value: null | [snapshotInstanceId: number, expIndex: number]): void {
+ const newRef = value && new RefProxy(value);
- if (!hasRefUnmount || value != null) {
- // Store the cleanup function on the function
- // instance object itself to avoid shape
- // transitioning vnode
- ref._unmount = ref(value);
- }
- } else ref.current = value;
-}
+ try {
+ if (typeof ref == 'function') {
+ const hasRefUnmount = typeof ref._unmount == 'function';
+ if (hasRefUnmount) {
+ ref._unmount!();
+ }
-function updateBackgroundRefs(commitId: number): void {
- const oldRefMap = globalRefsToRemove.get(commitId);
- if (oldRefMap) {
- globalRefsToRemove.delete(commitId);
- for (const ref of oldRefMap.values()) {
- applyRef(ref, null);
- }
- }
- const newRefMap = globalRefsToSet.get(commitId);
- if (newRefMap) {
- globalRefsToSet.delete(commitId);
- for (const sign in newRefMap) {
- const ref = backgroundSnapshotInstanceManager.getValueBySign(sign);
- if (ref) {
- // TODO: ref: support __REF_FIRE_IMMEDIATELY__
- const v = newRefMap[sign] && lynx.createSelectorQuery().selectUniqueID(newRefMap[sign]);
- applyRef(ref, v);
+ if (!hasRefUnmount || newRef != null) {
+ // Store the cleanup function on the function
+ // instance object itself to avoid shape
+ // transitioning vnode
+ ref._unmount = ref(newRef!);
}
- }
+ } else ref.current = newRef;
+ /* v8 ignore start */
+ } catch (e) {
+ lynx.reportError(e as Error);
}
+ /* v8 ignore stop */
}
function updateRef(
snapshot: SnapshotInstance,
expIndex: number,
- oldValue: any,
+ _oldValue: any,
elementIndex: number,
- spreadKey: string,
): void {
- const value = snapshot.__values![expIndex];
+ const value: unknown = snapshot.__values![expIndex];
let ref;
- if (!value) {
- ref = undefined;
- } else if (typeof value === 'string') {
+ if (typeof value === 'string') {
ref = value;
} else {
- ref = `${snapshot.__id}:${expIndex}:${spreadKey}`;
+ ref = `react-ref-${snapshot.__id}-${expIndex}`;
}
snapshot.__values![expIndex] = ref;
if (snapshot.__elements && ref) {
- __SetAttribute(snapshot.__elements[elementIndex]!, 'has-react-ref', true);
- const uid = __GetElementUniqueID(snapshot.__elements[elementIndex]!);
- globalRefPatch[ref] = uid;
- snapshot.__ref_set ??= new Set();
- snapshot.__ref_set.add(ref);
- }
- if (oldValue !== ref) {
- snapshot.__ref_set?.delete(oldValue);
+ __SetAttribute(snapshot.__elements[elementIndex]!, ref, 1);
}
}
-function takeGlobalRefPatchMap(): Record {
- const patch = globalRefPatch;
- globalRefPatch = {};
- return patch;
-}
-
-function transformRef(ref: unknown): Function | (object & Record<'current', unknown>) | null | undefined {
+function transformRef(ref: unknown): Ref | null | undefined {
if (ref === undefined || ref === null) {
return ref;
}
if (typeof ref === 'function' || (typeof ref === 'object' && 'current' in ref)) {
if ('__ref' in ref) {
- return ref;
+ return ref as Ref;
}
- return Object.defineProperty(ref, '__ref', { value: nextRefId++ });
+ return Object.defineProperty(ref, '__ref', { value: 1 }) as Ref;
}
throw new Error(
`Elements' "ref" property should be a function, or an object created `
@@ -124,25 +90,68 @@ function transformRef(ref: unknown): Function | (object & Record<'current', unkn
);
}
-function markRefToRemove(sign: string, ref: unknown): void {
- if (!ref) {
+/**
+ * Applies refs from a snapshot instance to their corresponding DOM elements.
+ *
+ * This function is called directly by preact with a `this` context of a Ref array that collects all
+ * refs that are applied during the process.
+ *
+ * @param snapshotInstance - The snapshot instance containing refs to apply
+ *
+ * If snapshotInstance is null, all previously collected refs are cleared.
+ * Otherwise, it iterates through the snapshot values, finds refs (either direct or in spreads),
+ * and applies them to their corresponding elements.
+ */
+function applyRefs(this: Ref[], snapshotInstance: BackgroundSnapshotInstance): void {
+ if (__LEPUS__) {
+ // for testing environment only
return;
}
- let oldRefs = globalRefsToRemove.get(nextCommitTaskId);
- if (!oldRefs) {
- oldRefs = new Map();
- globalRefsToRemove.set(nextCommitTaskId, oldRefs);
+ refsToApply.push(this, snapshotInstance);
+}
+
+function applyDelayedRefs(): void {
+ try {
+ for (let i = 0; i < refsToApply.length; i += 2) {
+ const refs = refsToApply[i] as Ref[];
+ const snapshotInstance = refsToApply[i + 1] as BackgroundSnapshotInstance;
+
+ if (snapshotInstance == null) {
+ try {
+ refs.forEach(ref => {
+ applyRef(ref, null);
+ });
+ } finally {
+ refs.length = 0;
+ }
+ continue;
+ }
+
+ for (let i = 0; i < snapshotInstance.__values!.length; i++) {
+ const value: unknown = snapshotInstance.__values![i];
+ if (!value || (typeof value !== 'function' && typeof value !== 'object')) {
+ continue;
+ }
+
+ let ref: Ref | undefined;
+ if ('__ref' in value) {
+ ref = value as Ref;
+ } else if ('__spread' in value) {
+ ref = (value as { ref?: Ref | undefined }).ref;
+ }
+
+ if (ref) {
+ applyRef(ref, [snapshotInstance.__id, i]);
+ refs.push(ref);
+ }
+ }
+ }
+ } finally {
+ refsToApply.length = 0;
}
- oldRefs.set(sign, ref);
}
-export {
- globalRefsToRemove,
- globalRefsToSet,
- markRefToRemove,
- takeGlobalRefPatchMap,
- transformRef,
- unref,
- updateBackgroundRefs,
- updateRef,
-};
+/**
+ * @internal
+ */
+export { updateRef, unref, transformRef, applyRef, applyRefs, applyDelayedRefs };
diff --git a/packages/react/runtime/src/snapshot/spread.ts b/packages/react/runtime/src/snapshot/spread.ts
index 678c39ea55..403b84f056 100644
--- a/packages/react/runtime/src/snapshot/spread.ts
+++ b/packages/react/runtime/src/snapshot/spread.ts
@@ -91,7 +91,6 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any,
} else if (key.startsWith('data-')) {
// collected below
} else if (key === 'ref') {
- snapshot.__ref_set ??= new Set();
const fakeSnapshot = {
__values: {
get [index]() {
@@ -104,9 +103,8 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any,
},
__id: snapshot.__id,
__elements: snapshot.__elements,
- __ref_set: snapshot.__ref_set,
} as SnapshotInstance;
- updateRef(fakeSnapshot, index, oldValue[key], elementIndex, key);
+ updateRef(fakeSnapshot, index, oldValue[key], elementIndex);
} else if (key.endsWith(':ref')) {
snapshot.__worklet_ref_set ??= new Set();
const fakeSnapshot = {
@@ -132,7 +130,7 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any,
__elements: snapshot.__elements,
} as SnapshotInstance;
updateGesture(fakeSnapshot, index, oldValue[key], elementIndex, workletType);
- } else if ((match = key.match(eventRegExp))) {
+ } else if ((match = eventRegExp.exec(key))) {
const workletType = match[2];
const eventType = eventTypeMap[match[3]!]!;
const eventName = match[4]!;
@@ -179,7 +177,6 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any,
} else if (key.startsWith('data-')) {
// collected below
} else if (key === 'ref') {
- snapshot.__ref_set ??= new Set();
const fakeSnapshot = {
__values: {
get [index]() {
@@ -192,9 +189,8 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any,
},
__id: snapshot.__id,
__elements: snapshot.__elements,
- __ref_set: snapshot.__ref_set,
} as SnapshotInstance;
- updateRef(fakeSnapshot, index, oldValue[key], elementIndex, key);
+ updateRef(fakeSnapshot, index, oldValue[key], elementIndex);
} else if (key.endsWith(':ref')) {
snapshot.__worklet_ref_set ??= new Set();
const fakeSnapshot = {
@@ -220,7 +216,7 @@ function updateSpread(snapshot: SnapshotInstance, index: number, oldValue: any,
__elements: snapshot.__elements,
} as SnapshotInstance;
updateGesture(fakeSnapshot, index, oldValue[key], elementIndex, workletType);
- } else if ((match = key.match(eventRegExp))) {
+ } else if ((match = eventRegExp.exec(key))) {
const workletType = match[2];
const eventType = eventTypeMap[match[3]!]!;
const eventName = match[4]!;
@@ -274,8 +270,12 @@ function transformSpread(
value ??= '';
result['className'] = value;
} else if (key === 'ref') {
- // @ts-ignore
- result[key] = transformRef(value)?.__ref;
+ if (__LEPUS__) {
+ result[key] = value ? 1 : undefined;
+ } else {
+ // @ts-ignore
+ result[key] = transformRef(value)?.__ref;
+ }
} else if (typeof value === 'function') {
result[key] = `${snapshot.__id}:${index}:${key}`;
} else {
diff --git a/packages/react/runtime/src/snapshot/workletRef.ts b/packages/react/runtime/src/snapshot/workletRef.ts
index 6628ef7dc1..5f55b84937 100644
--- a/packages/react/runtime/src/snapshot/workletRef.ts
+++ b/packages/react/runtime/src/snapshot/workletRef.ts
@@ -1,14 +1,10 @@
// Copyright 2024 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 {
- type Worklet,
- type WorkletRef,
- runWorkletCtx,
- updateWorkletRef as update,
-} from '@lynx-js/react/worklet-runtime/bindings';
+import { runWorkletCtx, updateWorkletRef as update } from '@lynx-js/react/worklet-runtime/bindings';
+import type { Worklet, WorkletRef } from '@lynx-js/react/worklet-runtime/bindings';
-import { SnapshotInstance } from '../snapshot.js';
+import type { SnapshotInstance } from '../snapshot.js';
function workletUnRef(value: Worklet | WorkletRef): void {
if ('_wvid' in value) {
@@ -42,10 +38,10 @@ function updateWorkletRef(
if (value === null || value === undefined) {
// do nothing
} else if (value._wvid) {
- update(value as any, snapshot.__elements[elementIndex]!);
+ update(value, snapshot.__elements[elementIndex]!);
} else if (value._wkltId) {
// @ts-ignore
- value._unmount = runWorkletCtx(value as any, [{ elementRefptr: snapshot.__elements[elementIndex]! }]);
+ value._unmount = runWorkletCtx(value, [{ elementRefptr: snapshot.__elements[elementIndex]! }]);
} else if (value._type === '__LEPUS__' || value._lepusWorkletHash) {
// During the initial render, we will not update the WorkletRef because the background thread is not ready yet.
} else {
diff --git a/packages/react/runtime/src/snapshotInstanceHydrationMap.ts b/packages/react/runtime/src/snapshotInstanceHydrationMap.ts
new file mode 100644
index 0000000000..9cddf69b3e
--- /dev/null
+++ b/packages/react/runtime/src/snapshotInstanceHydrationMap.ts
@@ -0,0 +1,17 @@
+// Copyright 2024 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.
+
+/**
+ * A map to store hydration states between snapshot instances.
+ * K->V: main thread snapshotInstance IDs -> background snapshotInstance IDs.
+ *
+ * The map is used by the ref system to translate between snapshot instance IDs when
+ * operations need to cross the thread boundary during the commit phase.
+ */
+const hydrationMap: Map = new Map();
+
+/**
+ * @internal
+ */
+export { hydrationMap };
diff --git a/packages/react/testing-library/src/__tests__/act.test.jsx b/packages/react/testing-library/src/__tests__/act.test.jsx
index e1bf4aec5e..ad324fc170 100644
--- a/packages/react/testing-library/src/__tests__/act.test.jsx
+++ b/packages/react/testing-library/src/__tests__/act.test.jsx
@@ -77,7 +77,7 @@ test('findByTestId returns the element', async () => {
Hello world!
@@ -88,7 +88,7 @@ test('findByTestId returns the element', async () => {
expect(await findByTestId('foo')).toMatchInlineSnapshot(`
Hello world!
@@ -96,12 +96,12 @@ test('findByTestId returns the element', async () => {
`);
expect(ref.current).toMatchInlineSnapshot(`
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "1",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
}
`);
});
diff --git a/packages/react/testing-library/src/__tests__/events.test.jsx b/packages/react/testing-library/src/__tests__/events.test.jsx
index 679c6f48f6..4dba02ef82 100644
--- a/packages/react/testing-library/src/__tests__/events.test.jsx
+++ b/packages/react/testing-library/src/__tests__/events.test.jsx
@@ -1,9 +1,10 @@
// cspell:disable
import '@testing-library/jest-dom';
-import { test } from 'vitest';
-import { fireEvent, render } from '..';
+import { expect, test, vi } from 'vitest';
+
import { createRef } from '@lynx-js/react';
-import { expect, vi } from 'vitest';
+
+import { fireEvent, render } from '..';
const eventTypes = [
{
@@ -74,29 +75,28 @@ eventTypes.forEach(({ type, events, elementType, init }, eventTypeIdx) => {
if (eventTypeIdx === 0 && eventIdx === 0) {
expect(ref).toMatchInlineSnapshot(`
{
- "current": NodesRef {
- "_nodeSelectToken": {
- "identifier": "1",
- "type": 2,
- },
- "_selectorQuery": {},
+ "current": RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
}
`);
expect(ref.current.constructor.name).toMatchInlineSnapshot(
- `"NodesRef"`,
- );
- const element = __GetElementByUniqueId(
- Number(ref.current._nodeSelectToken.identifier),
+ `"RefProxy"`,
);
+ const refId = `react-ref-${ref.current.refAttr[0]}-${ref.current.refAttr[1]}`;
+ const element = document.querySelector(`[${refId}]`);
expect(element).toMatchInlineSnapshot(`
`);
expect(element.attributes).toMatchInlineSnapshot(`
NamedNodeMap {
- "has-react-ref": "true",
+ "react-ref-2-0": "1",
}
`);
expect(element.eventMap).toMatchInlineSnapshot(`
diff --git a/packages/react/testing-library/src/__tests__/ref.test.jsx b/packages/react/testing-library/src/__tests__/ref.test.jsx
index f618a0030e..51664bf58a 100644
--- a/packages/react/testing-library/src/__tests__/ref.test.jsx
+++ b/packages/react/testing-library/src/__tests__/ref.test.jsx
@@ -51,10 +51,10 @@ describe('component ref', () => {
@@ -66,23 +66,23 @@ describe('component ref', () => {
expect(ref3.mock.calls).toMatchInlineSnapshot(`
[
[
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "3",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
`);
expect(ref4.current).toMatchInlineSnapshot(`
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "4",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 1,
+ ],
+ "task": undefined,
}
`);
expect(cleanup).toBeCalledTimes(0);
@@ -121,10 +121,10 @@ describe('element ref', () => {
@@ -132,23 +132,23 @@ describe('element ref', () => {
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "2",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
`);
expect(ref2.current).toMatchInlineSnapshot(`
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "3",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 1,
+ ],
+ "task": undefined,
}
`);
});
@@ -185,10 +185,10 @@ describe('element ref', () => {
@@ -196,23 +196,23 @@ describe('element ref', () => {
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "2",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
`);
expect(ref2.current).toMatchInlineSnapshot(`
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "3",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 1,
+ ],
+ "task": undefined,
}
`);
});
@@ -244,10 +244,10 @@ describe('element ref', () => {
@@ -255,23 +255,23 @@ describe('element ref', () => {
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "2",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
`);
expect(ref2.current).toMatchInlineSnapshot(`
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "3",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 1,
+ ],
+ "task": undefined,
}
`);
act(() => {
@@ -281,12 +281,12 @@ describe('element ref', () => {
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "2",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
[
@@ -328,7 +328,7 @@ describe('element ref', () => {
@@ -336,12 +336,12 @@ describe('element ref', () => {
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "2",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
@@ -355,12 +355,12 @@ describe('element ref', () => {
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "2",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
@@ -406,10 +406,10 @@ describe('element ref', () => {
@@ -417,23 +417,23 @@ describe('element ref', () => {
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "2",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
`);
expect(ref2.current).toMatchInlineSnapshot(`
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "3",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 1,
+ ],
+ "task": undefined,
}
`);
expect(lynx.getNativeApp().callLepusMethod).toBeCalledTimes(1);
@@ -441,12 +441,12 @@ describe('element ref', () => {
expect(ref1.mock.calls).toMatchInlineSnapshot(`
[
[
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "2",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
},
],
]
diff --git a/packages/react/testing-library/src/__tests__/render.test.jsx b/packages/react/testing-library/src/__tests__/render.test.jsx
index cfd62ca70a..60fcef559e 100644
--- a/packages/react/testing-library/src/__tests__/render.test.jsx
+++ b/packages/react/testing-library/src/__tests__/render.test.jsx
@@ -17,12 +17,12 @@ test('renders view into page', async () => {
};
render();
expect(ref.current).toMatchInlineSnapshot(`
- NodesRef {
- "_nodeSelectToken": {
- "identifier": "1",
- "type": 2,
- },
- "_selectorQuery": {},
+ RefProxy {
+ "refAttr": [
+ 2,
+ 0,
+ ],
+ "task": undefined,
}
`);
});
diff --git a/packages/react/testing-library/src/fire-event.ts b/packages/react/testing-library/src/fire-event.ts
index 602c9e8e7d..52f784d89d 100644
--- a/packages/react/testing-library/src/fire-event.ts
+++ b/packages/react/testing-library/src/fire-event.ts
@@ -1,12 +1,14 @@
// @ts-nocheck
-import { fireEvent as domFireEvent, createEvent } from '@testing-library/dom';
+import { createEvent, fireEvent as domFireEvent } from '@testing-library/dom';
-let NodesRef = lynx.createSelectorQuery().selectUniqueID(-1).constructor;
+const NodesRef = lynx.createSelectorQuery().selectUniqueID(-1).constructor;
function getElement(elemOrNodesRef) {
if (elemOrNodesRef instanceof NodesRef) {
return __GetElementByUniqueId(
Number(elemOrNodesRef._nodeSelectToken.identifier),
);
+ } else if ('refAttr' in elemOrNodesRef) {
+ return document.querySelector(`[react-ref-${elemOrNodesRef.refAttr[0]}-${elemOrNodesRef.refAttr[1]}]`);
} else if (elemOrNodesRef?.constructor?.name === 'HTMLUnknownElement') {
return elemOrNodesRef;
} else {
@@ -25,7 +27,7 @@ export const fireEvent: any = (elemOrNodesRef, ...args) => {
const elem = getElement(elemOrNodesRef);
- let ans = domFireEvent(elem, ...args);
+ const ans = domFireEvent(elem, ...args);
if (isMainThread) {
// switch back to main thread
@@ -156,7 +158,7 @@ Object.keys(eventMap).forEach((key) => {
lynxTestingEnv.switchToBackgroundThread();
const elem = getElement(elemOrNodesRef);
- const eventType = init?.['eventType'] || 'bindEvent';
+ const eventType = init?.eventType || 'bindEvent';
init = {
eventType,
eventName: key,
diff --git a/packages/react/transform/src/swc_plugin_snapshot/mod.rs b/packages/react/transform/src/swc_plugin_snapshot/mod.rs
index 86f80ae751..ab4523d62c 100644
--- a/packages/react/transform/src/swc_plugin_snapshot/mod.rs
+++ b/packages/react/transform/src/swc_plugin_snapshot/mod.rs
@@ -226,7 +226,7 @@ impl DynamicPart {
element_index: Expr = i32_to_expr(element_index),
),
AttrName::Ref => quote!(
- "(snapshot, index, oldValue) => $runtime_id.updateRef(snapshot, index, oldValue, $element_index, '')" as Expr,
+ "(snapshot, index, oldValue) => $runtime_id.updateRef(snapshot, index, oldValue, $element_index)" as Expr,
runtime_id: Expr = runtime_id.clone(),
element_index: Expr = i32_to_expr(element_index),
),
@@ -1306,6 +1306,8 @@ where
let mut snapshot_values: Vec