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

Filter by extension

Filter by extension

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

fix: `ref is not initialized` error on template reload
14 changes: 7 additions & 7 deletions packages/react/runtime/__test__/snapshot/workletRef.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import { __root } from '../../src/root';
import { setupPage } from '../../src/snapshot';
import { globalEnvManager } from '../utils/envManager';
import { elementTree } from '../utils/nativeMethod';
import { injectUpdateMTRefInitValue } from '../../src/worklet/ref/updateInitValue';

beforeAll(() => {
setupPage(__CreatePage('0', 0));
injectUpdateMainThread();
injectUpdateMTRefInitValue();
replaceCommitHook();

globalThis.lynxWorkletImpl = {
Expand Down Expand Up @@ -268,14 +270,10 @@ describe('WorkletRef', () => {
expect(lynx.getNativeApp().callLepusMethod.mock.calls).toMatchInlineSnapshot(`
[
[
"rLynxChange",
"rLynxChangeRefInitValue",
{
"data": "{"patchList":[{"id":5,"workletRefInitValuePatch":[[1,null],[2,null],[3,null],[4,null],[5,null],[6,null]]}]}",
"patchOptions": {
"reloadVersion": 0,
},
"data": "[[1,null],[2,null],[3,null],[4,null],[5,null],[6,null]]",
},
[Function],
],
[
"rLynxChange",
Expand Down Expand Up @@ -497,7 +495,9 @@ describe('WorkletRef', () => {
render([createCompBG1('ref1'), createCompBG1('mts')], __root);

globalEnvManager.switchToMainThread();
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
let rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[0];
globalThis[rLynxChange[0]](rLynxChange[1]);
rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls[1];
globalThis[rLynxChange[0]](rLynxChange[1]);
expect(__root.__element_root).toMatchInlineSnapshot(`
<page
Expand Down
83 changes: 72 additions & 11 deletions packages/react/runtime/__test__/worklet/workletRef.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@ import { destroyWorklet } from '../../src/worklet/destroy';
import { clearConfigCacheForTesting } from '../../src/worklet/functionality';
import { MainThreadRef, useMainThreadRef } from '../../src/worklet/ref/workletRef';
import { globalEnvManager } from '../utils/envManager';
import { injectUpdateMTRefInitValue } from '../../src/worklet/ref/updateInitValue';

const Comp = () => {
const ref = useMainThreadRef(233);
return <view></view>;
};

beforeAll(() => {
setupPage(__CreatePage('0', 0));
injectUpdateMainThread();
injectUpdateMTRefInitValue();
replaceCommitHook();
globalThis.lynxWorkletImpl = {
_refImpl: {
Expand Down Expand Up @@ -71,11 +78,6 @@ describe('WorkletRef in js', () => {
});

it('should send init value to the main thread', () => {
const Comp = () => {
const ref = useMainThreadRef(233);
return <view></view>;
};

// main thread render
{
__root.__jsx = <Comp />;
Expand All @@ -99,14 +101,10 @@ describe('WorkletRef in js', () => {
expect(rLynxChange).toMatchInlineSnapshot(`
[
[
"rLynxChange",
"rLynxChangeRefInitValue",
{
"data": "{"patchList":[{"id":1,"workletRefInitValuePatch":[[1,233]]}]}",
"patchOptions": {
"reloadVersion": 0,
},
"data": "[[1,233]]",
},
[Function],
],
[
"rLynxChange",
Expand Down Expand Up @@ -216,4 +214,67 @@ describe('WorkletRef in js', () => {
`);
}
});

it('should send init value to the main thread even after reloadTemplate', () => {
// main thread render
{
__root.__jsx = <Comp />;
renderPage();
}

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

// main thread reload
{
globalEnvManager.switchToMainThread();
updatePage({}, { reloadTemplate: true });
}

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

// rLynxChange
globalEnvManager.switchToMainThread();
const rLynxChange = lynx.getNativeApp().callLepusMethod.mock.calls;
expect(rLynxChange).toMatchInlineSnapshot(`
[
[
"rLynxChangeRefInitValue",
{
"data": "[[1,233]]",
},
],
[
"rLynxChange",
{
"data": "{"patchList":[{"snapshotPatch":[],"id":6}]}",
"patchOptions": {
"isHydration": true,
"pipelineOptions": {
"dsl": "reactLynx",
"needTimestamps": true,
"pipelineID": "pipelineID",
"pipelineOrigin": "reactLynxHydrate",
"stage": "hydrate",
},
"reloadVersion": 1,
},
},
[Function],
],
]
`);
globalThis[rLynxChange[0][0]](rLynxChange[0][1]);
expect(globalThis.lynxWorkletImpl._refImpl.updateWorkletRefInitValueChanges).toBeCalledTimes(1);
globalThis[rLynxChange[1][0]](rLynxChange[1][1]);
expect(globalThis.lynxWorkletImpl._refImpl.updateWorkletRefInitValueChanges).toBeCalledTimes(1);
}
});
});
11 changes: 4 additions & 7 deletions packages/react/runtime/src/lifecycle/patch/commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { COMMIT } from '../../renderToOpcodes/constants.js';
import { applyQueuedRefs } from '../../snapshot/ref.js';
import { backgroundSnapshotInstanceManager } from '../../snapshot.js';
import { hook, isEmptyObject } from '../../utils.js';
import { takeWorkletRefInitValuePatch } from '../../worklet/ref/workletRefPool.js';
import { sendMTRefInitValueToMainThread } from '../../worklet/ref/updateInitValue.js';
import { getReloadVersion } from '../pass.js';
import type { SnapshotPatch } from './snapshotPatch.js';
import { takeGlobalSnapshotPatch } from './snapshotPatch.js';
Expand Down Expand Up @@ -60,7 +60,6 @@ interface Patch {
// TODO: ref: do we need `id`?
id: number;
snapshotPatch?: SnapshotPatch;
workletRefInitValuePatch?: [id: number, value: unknown][];
}

/**
Expand Down Expand Up @@ -135,12 +134,13 @@ function replaceCommitHook(): void {
}
});

sendMTRefInitValueToMainThread();

// Collect patches for this update
const snapshotPatch = takeGlobalSnapshotPatch();
const flushOptions = takeGlobalFlushOptions();
const patchOptions = takeGlobalPatchOptions();
const workletRefInitValuePatch = takeWorkletRefInitValuePatch();
if (!snapshotPatch && workletRefInitValuePatch.length === 0) {
if (!snapshotPatch) {
// before hydration, skip patch
applyQueuedRefs();
originalPreactCommit?.(vnode, commitQueue);
Expand All @@ -154,9 +154,6 @@ function replaceCommitHook(): void {
if (snapshotPatch?.length) {
patch.snapshotPatch = snapshotPatch;
}
if (workletRefInitValuePatch.length) {
patch.workletRefInitValuePatch = workletRefInitValuePatch;
}
const patchList: PatchList = {
patchList: [patch],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
// LICENSE file in the root directory of this source tree.

import type { ClosureValueType } from '@lynx-js/react/worklet-runtime/bindings';
import {
runRunOnMainThreadTask,
setEomShouldFlushElementTree,
updateWorkletRefInitValueChanges,
} from '@lynx-js/react/worklet-runtime/bindings';
import { runRunOnMainThreadTask, setEomShouldFlushElementTree } from '@lynx-js/react/worklet-runtime/bindings';

import type { PatchList, PatchOptions } from './commit.js';
import { setMainThreadHydrating } from './isMainThreadHydrating.js';
Expand Down Expand Up @@ -50,8 +46,7 @@ function updateMainThread(
setMainThreadHydrating(true);
}
try {
for (const { snapshotPatch, workletRefInitValuePatch } of patchList) {
updateWorkletRefInitValueChanges(workletRefInitValuePatch);
for (const { snapshotPatch } of patchList) {
__pendingListUpdates.clearAttachedLists();
if (snapshotPatch) {
snapshotPatchApply(snapshotPatch);
Expand Down
1 change: 1 addition & 0 deletions packages/react/runtime/src/lifecycleConstant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const enum LifecycleConstant {
jsReady = 'rLynxJSReady',
patchUpdate = 'rLynxChange',
publishEvent = 'rLynxPublishEvent',
updateMTRefInitValue = 'rLynxChangeRefInitValue',
}

export interface FirstScreenData {
Expand Down
2 changes: 2 additions & 0 deletions packages/react/runtime/src/lynx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { injectLepusMethods } from './lynx/injectLepusMethods.js';
import { initTimingAPI } from './lynx/performance.js';
import { injectTt } from './lynx/tt.js';
import { lynxQueueMicrotask } from './utils.js';
import { injectUpdateMTRefInitValue } from './worklet/ref/updateInitValue.js';

export { runWithForce } from './lynx/runWithForce.js';

Expand All @@ -32,6 +33,7 @@ if (__MAIN_THREAD__ && typeof globalThis.processEvalResult === 'undefined') {
if (__MAIN_THREAD__) {
injectCalledByNative();
injectUpdateMainThread();
injectUpdateMTRefInitValue();
if (__DEV__) {
injectLepusMethods();
}
Expand Down
3 changes: 2 additions & 1 deletion packages/react/runtime/src/lynx/tt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
takeDelayedRunOnMainThreadData,
} from '../worklet/call/delayedRunOnMainThreadData.js';
import { destroyWorklet } from '../worklet/destroy.js';
import { sendMTRefInitValueToMainThread } from '../worklet/ref/updateInitValue.js';

export { runWithForce };

Expand Down Expand Up @@ -128,7 +129,7 @@ function onLifecycleEventImpl(type: LifecycleConstant, data: unknown): void {
patchList.delayedRunOnMainThreadData = takeDelayedRunOnMainThreadData();
}
const obj = commitPatchUpdate(patchList, { isHydration: true });

sendMTRefInitValueToMainThread();
lynx.getNativeApp().callLepusMethod(LifecycleConstant.patchUpdate, obj, () => {
globalCommitTaskMap.forEach((commitTask, id) => {
if (id > commitTaskId) {
Expand Down
30 changes: 30 additions & 0 deletions packages/react/runtime/src/worklet/ref/updateInitValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2025 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 { updateWorkletRefInitValueChanges } from '@lynx-js/react/worklet-runtime/bindings';

import { takeWorkletRefInitValuePatch } from './workletRefPool.js';
import type { workletRefInitValuePatch } from './workletRefPool.js';
import { LifecycleConstant } from '../../lifecycleConstant.js';

function updateMTRefInitValue({ data }: { data: string }): void {
// This update ignores reloadVersion check.
// MainThreadRefs created before reloadTemplate may still be referenced by user in some cases after reloadTemplate.
const patch = JSON.parse(data) as workletRefInitValuePatch;
updateWorkletRefInitValueChanges(patch);
}

export function injectUpdateMTRefInitValue(): void {
Object.assign(globalThis, { [LifecycleConstant.updateMTRefInitValue]: updateMTRefInitValue });
}

export function sendMTRefInitValueToMainThread(): void {
const patch = takeWorkletRefInitValuePatch();
if (patch.length === 0) {
return;
}

const data = JSON.stringify(patch);
lynx.getNativeApp().callLepusMethod(LifecycleConstant.updateMTRefInitValue, { data });
}
8 changes: 4 additions & 4 deletions packages/react/runtime/src/worklet/ref/workletRefPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

import { isMtsEnabled } from '../functionality.js';

let initValuePatch: [number, unknown][] = [];
const initValueIdSet = /*#__PURE__*/ new Set<number>();
export type workletRefInitValuePatch = [id: number, value: unknown][];

let initValuePatch: workletRefInitValuePatch = [];

/**
* @internal
Expand All @@ -15,14 +16,13 @@ export function addWorkletRefInitValue(id: number, value: unknown): void {
return;
}

initValueIdSet.add(id);
initValuePatch.push([id, value]);
}

/**
* @internal
*/
export function takeWorkletRefInitValuePatch(): [number, unknown][] {
export function takeWorkletRefInitValuePatch(): workletRefInitValuePatch {
const res = initValuePatch;
initValuePatch = [];
return res;
Expand Down
15 changes: 2 additions & 13 deletions packages/react/testing-library/src/__tests__/worklet.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -439,21 +439,10 @@ describe('worklet', () => {
expect(callLepusMethodCalls).toMatchInlineSnapshot(`
[
[
"rLynxChange",
"rLynxChangeRefInitValue",
{
"data": "{"patchList":[{"id":1,"workletRefInitValuePatch":[[1,null],[2,0]]}]}",
"patchOptions": {
"pipelineOptions": {
"dsl": "reactLynx",
"needTimestamps": true,
"pipelineID": "pipelineID",
"pipelineOrigin": "reactLynxHydrate",
"stage": "hydrate",
},
"reloadVersion": 0,
},
"data": "[[1,null],[2,0]]",
},
[Function],
],
[
"rLynxChange",
Expand Down
2 changes: 2 additions & 0 deletions packages/react/testing-library/src/vitest-global-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BackgroundSnapshotInstance } from '../../runtime/lib/backgroundSnapshot
import { clearCommitTaskId, replaceCommitHook } from '../../runtime/lib/lifecycle/patch/commit.js';
import { deinitGlobalSnapshotPatch } from '../../runtime/lib/lifecycle/patch/snapshotPatch.js';
import { injectUpdateMainThread } from '../../runtime/lib/lifecycle/patch/updateMainThread.js';
import { injectUpdateMTRefInitValue } from '../../runtime/lib/worklet/ref/updateInitValue.js';
import { injectCalledByNative } from '../../runtime/lib/lynx/calledByNative.js';
import { flushDelayedLifecycleEvents, injectTt } from '../../runtime/lib/lynx/tt.js';
import { setRoot } from '../../runtime/lib/root.js';
Expand All @@ -28,6 +29,7 @@ const {

injectCalledByNative();
injectUpdateMainThread();
injectUpdateMTRefInitValue();
replaceCommitHook();

globalThis.onInitWorkletRuntime = () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/testing-library/testing-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function createPolyfills() {
globalThis[rLynxChange[0]](rLynxChange[1]);

globalThis.lynxTestingEnv.switchToBackgroundThread();
rLynxChange[2]();
rLynxChange[2]?.();
globalThis.lynxTestingEnv.switchToMainThread();

// restore the original thread state
Expand Down
Loading