diff --git a/.changeset/violet-wasps-shop.md b/.changeset/violet-wasps-shop.md new file mode 100644 index 0000000000..7e48ce63a2 --- /dev/null +++ b/.changeset/violet-wasps-shop.md @@ -0,0 +1,5 @@ +--- +"@lynx-js/react": patch +--- + +fix: `ref is not initialized` error on template reload diff --git a/packages/react/runtime/__test__/snapshot/workletRef.test.jsx b/packages/react/runtime/__test__/snapshot/workletRef.test.jsx index 612dd2e1de..f5bc392b3c 100644 --- a/packages/react/runtime/__test__/snapshot/workletRef.test.jsx +++ b/packages/react/runtime/__test__/snapshot/workletRef.test.jsx @@ -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 = { @@ -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", @@ -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(` { + const ref = useMainThreadRef(233); + return ; +}; beforeAll(() => { setupPage(__CreatePage('0', 0)); injectUpdateMainThread(); + injectUpdateMTRefInitValue(); replaceCommitHook(); globalThis.lynxWorkletImpl = { _refImpl: { @@ -71,11 +78,6 @@ describe('WorkletRef in js', () => { }); it('should send init value to the main thread', () => { - const Comp = () => { - const ref = useMainThreadRef(233); - return ; - }; - // main thread render { __root.__jsx = ; @@ -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", @@ -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 = ; + renderPage(); + } + + // background render + { + globalEnvManager.switchToBackground(); + render(, __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); + } + }); }); diff --git a/packages/react/runtime/src/lifecycle/patch/commit.ts b/packages/react/runtime/src/lifecycle/patch/commit.ts index 17b771120f..b0bf145f63 100644 --- a/packages/react/runtime/src/lifecycle/patch/commit.ts +++ b/packages/react/runtime/src/lifecycle/patch/commit.ts @@ -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'; @@ -60,7 +60,6 @@ interface Patch { // TODO: ref: do we need `id`? id: number; snapshotPatch?: SnapshotPatch; - workletRefInitValuePatch?: [id: number, value: unknown][]; } /** @@ -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); @@ -154,9 +154,6 @@ function replaceCommitHook(): void { if (snapshotPatch?.length) { patch.snapshotPatch = snapshotPatch; } - if (workletRefInitValuePatch.length) { - patch.workletRefInitValuePatch = workletRefInitValuePatch; - } const patchList: PatchList = { patchList: [patch], }; diff --git a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts index c15ce37f1d..0a5e682b2f 100644 --- a/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts +++ b/packages/react/runtime/src/lifecycle/patch/updateMainThread.ts @@ -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'; @@ -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); diff --git a/packages/react/runtime/src/lifecycleConstant.ts b/packages/react/runtime/src/lifecycleConstant.ts index 34a8783b61..3cdea6198f 100644 --- a/packages/react/runtime/src/lifecycleConstant.ts +++ b/packages/react/runtime/src/lifecycleConstant.ts @@ -8,6 +8,7 @@ export const enum LifecycleConstant { jsReady = 'rLynxJSReady', patchUpdate = 'rLynxChange', publishEvent = 'rLynxPublishEvent', + updateMTRefInitValue = 'rLynxChangeRefInitValue', } export interface FirstScreenData { diff --git a/packages/react/runtime/src/lynx.ts b/packages/react/runtime/src/lynx.ts index d990daed11..9ae7a65905 100644 --- a/packages/react/runtime/src/lynx.ts +++ b/packages/react/runtime/src/lynx.ts @@ -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'; @@ -32,6 +33,7 @@ if (__MAIN_THREAD__ && typeof globalThis.processEvalResult === 'undefined') { if (__MAIN_THREAD__) { injectCalledByNative(); injectUpdateMainThread(); + injectUpdateMTRefInitValue(); if (__DEV__) { injectLepusMethods(); } diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index f38d7098d0..73788535f4 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -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 }; @@ -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) { diff --git a/packages/react/runtime/src/worklet/ref/updateInitValue.ts b/packages/react/runtime/src/worklet/ref/updateInitValue.ts new file mode 100644 index 0000000000..f547b3228d --- /dev/null +++ b/packages/react/runtime/src/worklet/ref/updateInitValue.ts @@ -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 }); +} diff --git a/packages/react/runtime/src/worklet/ref/workletRefPool.ts b/packages/react/runtime/src/worklet/ref/workletRefPool.ts index b6dc75a98c..f390bfdf70 100644 --- a/packages/react/runtime/src/worklet/ref/workletRefPool.ts +++ b/packages/react/runtime/src/worklet/ref/workletRefPool.ts @@ -4,8 +4,9 @@ import { isMtsEnabled } from '../functionality.js'; -let initValuePatch: [number, unknown][] = []; -const initValueIdSet = /*#__PURE__*/ new Set(); +export type workletRefInitValuePatch = [id: number, value: unknown][]; + +let initValuePatch: workletRefInitValuePatch = []; /** * @internal @@ -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; diff --git a/packages/react/testing-library/src/__tests__/worklet.test.jsx b/packages/react/testing-library/src/__tests__/worklet.test.jsx index 23400559e7..ac02d55fac 100644 --- a/packages/react/testing-library/src/__tests__/worklet.test.jsx +++ b/packages/react/testing-library/src/__tests__/worklet.test.jsx @@ -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", diff --git a/packages/react/testing-library/src/vitest-global-setup.js b/packages/react/testing-library/src/vitest-global-setup.js index 0e1a451f53..990efe308e 100644 --- a/packages/react/testing-library/src/vitest-global-setup.js +++ b/packages/react/testing-library/src/vitest-global-setup.js @@ -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'; @@ -28,6 +29,7 @@ const { injectCalledByNative(); injectUpdateMainThread(); +injectUpdateMTRefInitValue(); replaceCommitHook(); globalThis.onInitWorkletRuntime = () => { diff --git a/packages/testing-library/testing-environment/src/index.ts b/packages/testing-library/testing-environment/src/index.ts index cf7111bd6e..cec0a138cd 100644 --- a/packages/testing-library/testing-environment/src/index.ts +++ b/packages/testing-library/testing-environment/src/index.ts @@ -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