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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/petite-bobcats-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@lynx-js/react": patch
---

fix: ensure ref lifecycle events run after firstScreen in the background thread

This patch fixes an issue where ref lifecycle events were running before firstScreen events in the background thread async render mode, which could cause refs to be undefined when components try to access them.
Comment thread
Yradex marked this conversation as resolved.
9 changes: 9 additions & 0 deletions packages/react/runtime/__test__/lifecycle/reload.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
import { BasicBG, ListBG, ListConditionalBG, ViewBG, setObj, setStr } from './reloadBG';
import { BasicMT, ListConditionalMT, ListMT, ViewMT } from './reloadMT';
import { root } from '../../src/index';
import { delayedEvents, delayedPublishEvent } from '../../src/lifecycle/event/delayEvents';
import { replaceCommitHook } from '../../src/lifecycle/patch/commit';
import { injectUpdateMainThread } from '../../src/lifecycle/patch/updateMainThread';
import { reloadBackground } from '../../src/lifecycle/reload';
import { __root } from '../../src/root';
import { setupPage } from '../../src/snapshot';
import { globalEnvManager } from '../utils/envManager';
Expand Down Expand Up @@ -1882,4 +1884,11 @@ describe('firstScreenSyncTiming - jsReady', () => {
lynx.getNativeApp().callLepusMethod.mockClear();
}
});

it('should clear cached events before reload when js not ready', async function() {
delayedPublishEvent('bindEvent:tap', 'test');
expect(delayedEvents.length).toBe(1);
reloadBackground({});
expect(delayedEvents.length).toBe(0);
});
});
104 changes: 103 additions & 1 deletion packages/react/runtime/__test__/snapshot/ref.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
*/
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

import { Component, createRef, useState } from '../../src/index';
import { Component, createRef, root, useState } from '../../src/index';
import { delayedLifecycleEvents } from '../../src/lifecycle/event/delayLifecycleEvents';
import { replaceCommitHook } from '../../src/lifecycle/patch/commit';
import { injectUpdateMainThread } from '../../src/lifecycle/patch/updateMainThread';
import { renderBackground as render } from '../../src/lifecycle/render';
Expand Down Expand Up @@ -1650,4 +1651,105 @@ describe('element ref in list', () => {
`);
}
});

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 <view ref={this.props._ref}></view>;
}
}

class Comp extends Component {
render() {
return (
<list>
{[0, 1, 2].map((index) => {
return (
<list-item item-key={index}>
<ListItem _ref={refs[index]}></ListItem>
</list-item>
);
})}
</list>
);
}
}

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

// list render item 1 & 2
{
signs[0] = elementTree.triggerComponentAtIndex(__root.childNodes[0].__elements[0], 0);
expect(globalThis.__OnLifecycleEvent).toHaveBeenCalledTimes(1);

globalEnvManager.switchToBackground();
lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
globalThis.__OnLifecycleEvent.mockClear();
expect(delayedLifecycleEvents).toMatchInlineSnapshot(`
[
[
"rLynxRef",
{
"commitTaskId": undefined,
"refPatch": "{"-4:0:":62}",
},
],
]
`);
}

// background render
{
globalEnvManager.switchToBackground();
root.render(<Comp />, __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]);
lynx.getNativeApp().callLepusMethod.mockClear();
expect(globalThis.__OnLifecycleEvent).toHaveBeenCalledTimes(1);
expect(globalThis.__OnLifecycleEvent.mock.calls).toMatchInlineSnapshot(`
[
[
[
"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:"]}]}]}]}",
},
],
],
]
`);
}

{
globalEnvManager.switchToBackground();
lynxCoreInject.tt.OnLifecycleEvent(...globalThis.__OnLifecycleEvent.mock.calls[0]);
globalThis.__OnLifecycleEvent.mockClear();

expect(refs[0].current).toMatchInlineSnapshot(`
{
"selectUniqueID": [Function],
"uid": 62,
}
`);
}
globalThis.__FIRST_SCREEN_SYNC_TIMING__ = 'immediately';
});
});
9 changes: 8 additions & 1 deletion packages/react/runtime/src/lifecycle/destroy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// 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 { __root } from '../root.js';
import { delayedEvents } from './event/delayEvents.js';
import { delayedLifecycleEvents } from './event/delayLifecycleEvents.js';
import { globalCommitTaskMap } from './patch/commit.js';
import { renderBackground } from './render.js';

Expand All @@ -16,7 +18,12 @@ function destroyBackground(): void {
task();
});
globalCommitTaskMap.clear();

// Clear delayed events which should not be executed after destroyed.
// This is important when the page is performing a reload.
delayedLifecycleEvents.length = 0;
if (delayedEvents) {
delayedEvents.length = 0;
}
if (__PROFILE__) {
console.profileEnd();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { LifecycleConstant } from '../../lifecycleConstant.js';

const delayedLifecycleEvents: [type: string, data: any][] = [];

function delayLifecycleEvent(type: string, data: any): void {
delayedLifecycleEvents.push([type, data]);
// 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]);
}
}

/**
Expand Down
9 changes: 5 additions & 4 deletions packages/react/runtime/src/lynx-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,13 @@ export const root: Root = {
} else {
__root.__jsx = jsx;
renderBackground(jsx, __root as any);
if (__FIRST_SCREEN_SYNC_TIMING__ === 'immediately') {}
else {
if (__FIRST_SCREEN_SYNC_TIMING__ === 'immediately') {
// This is for cases where `root.render()` is called asynchronously,
// `firstScreen` message might have been reached.
flushDelayedLifecycleEvents();
} else {
lynx.getNativeApp().callLepusMethod(LifecycleConstant.jsReady, {});
}

flushDelayedLifecycleEvents();
}
},
registerDataProcessors: (dataProcessorDefinition: DataProcessorDefinition): void => {
Expand Down
3 changes: 3 additions & 0 deletions packages/react/runtime/src/lynx/tt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ async function onLifecycleEventImpl(type: string, data: any): Promise<void> {
}
markTiming(PerformanceTimingKeys.diff_vdom_end);

// TODO: It seems `delayedEvents` and `delayedLifecycleEvents` should be merged into one array to ensure the proper order of events.
flushDelayedLifecycleEvents();
if (delayedEvents) {
delayedEvents.forEach((args) => {
const [handlerName, data] = args;
Expand All @@ -144,6 +146,7 @@ async function onLifecycleEventImpl(type: string, data: any): Promise<void> {
});
delayedEvents.length = 0;
}

lynxCoreInject.tt.publishEvent = publishEvent;
lynxCoreInject.tt.publicComponentEvent = publicComponentEvent;

Expand Down
Loading