Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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/tall-seals-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/react": patch
---

Fix `cloneElement` and `createElement` not working correctly on the main thread.
7 changes: 2 additions & 5 deletions packages/react/components/src/DeferredListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@

import type { FC, ReactNode, RefCallback } from 'react';

import { cloneElement as _cloneElement, useCallback, useRef, useState } from '@lynx-js/react';
import { cloneElement, useCallback, useRef, useState } from '@lynx-js/react';
import type { SnapshotInstance } from '@lynx-js/react/internal';
import { cloneElement as _cloneElementMainThread } from '@lynx-js/react/lepus';

export interface DeferredListItemProps {
defer?: boolean | { unmountRecycled?: boolean };
Expand All @@ -15,8 +14,6 @@ export interface DeferredListItemProps {
}

export const DeferredListItem: FC<DeferredListItemProps> = ({ defer, renderListItem, renderChildren }) => {
const __cloneElement = __MAIN_THREAD__ ? _cloneElementMainThread : _cloneElement;

const initialDeferRef = useRef(defer);
const prevDeferRef = useRef(defer);
const [isReady, setIsReady] = useState(!defer);
Expand Down Expand Up @@ -50,7 +47,7 @@ export const DeferredListItem: FC<DeferredListItemProps> = ({ defer, renderListI
}

return initialDeferRef.current
? __cloneElement(renderListItem(isReady ? renderChildren() : null), {
? cloneElement(renderListItem(isReady ? renderChildren() : null), {
isReady: +isReady, // hack: preact specially handled boolean props
ref: onGetSnapshotInstance,
})
Expand Down
2 changes: 1 addition & 1 deletion packages/react/runtime/lepus/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// 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.
export { cloneElement, createElement } from 'react';
export { cloneElement, createElement } from 'preact/compat';
15 changes: 15 additions & 0 deletions packages/react/runtime/src/cloneElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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 { cloneElement as preactCloneElement } from 'preact/compat';

import { cloneElement as mainThreadCloneElement } from '../lepus/index.js';

export const cloneElement: typeof import('preact/compat').cloneElement = /*#__PURE__*/ (() => {
if (__BACKGROUND__) {
return preactCloneElement;
} else {
return mainThreadCloneElement;
}
})();
14 changes: 14 additions & 0 deletions packages/react/runtime/src/createElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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 { createElement as preactCreateElement } from 'preact/compat';

import { createElement as mainThreadCreateElement } from '../lepus/index.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify lepus.createElement also accepts and forwards variadic children

To maintain parity, ensure createElement in lepus uses ...children and forwards them.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "createElement signature in lepus runtime:"
rg -nP -C2 'export\s+function\s+createElement\s*\(' packages/react/runtime/lepus/index.js || true

echo
echo "Look for rest-children usage and forwarding:"
rg -nP -C2 '\.\.\.children|createElement\([^)]*,[^)]*,\s*\.\.\.children' packages/react/runtime/lepus/index.js || true

Length of output: 562


Action: Make lepus.createElement accept and forward variadic children

Found: packages/react/runtime/lepus/index.js declares
export function createElement(type, props, children) { … } (no variadic forwarding found).

What to change:

  • Update signature to accept rest children and forward them to the underlying VNode creation/calls.
  • File to modify:
    • packages/react/runtime/lepus/index.js — change the function signature and ensure any internal calls use ...children.

Suggested diff:

  • packages/react/runtime/lepus/index.js
  • export function createElement(type, props, children) {
  • export function createElement(type, props, ...children) {
🤖 Prompt for AI Agents
packages/react/runtime/lepus/index.js (createElement export) — The createElement
currently takes (type, props, children); change its signature to accept rest
children (type, props, ...children) and update any internal calls that build or
forward VNodes so they use ...children when invoking downstream creation
functions or constructing children arrays; ensure places that assumed a single
children value handle an array of children or spread appropriately and preserve
existing behavior for null/undefined children.


export const createElement: typeof import('preact/compat').createElement = /*#__PURE__*/ (() => {
if (__BACKGROUND__) {
return preactCreateElement;
} else {
return mainThreadCreateElement;
}
})();
5 changes: 3 additions & 2 deletions packages/react/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import {
Component,
Fragment,
PureComponent,
cloneElement,
createContext,
createElement,
createRef,
forwardRef,
isValidElement,
Expand All @@ -20,6 +18,8 @@ import {
useSyncExternalStore,
} from 'preact/compat';

import { cloneElement } from './cloneElement.js';
import { createElement } from './createElement.js';
import {
useCallback,
useContext,
Expand Down Expand Up @@ -72,6 +72,7 @@ export default {
forwardRef,
Suspense,
lazy,
cloneElement,
createElement,
};

Expand Down
13 changes: 5 additions & 8 deletions packages/react/runtime/src/lynx/suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,20 @@
// LICENSE file in the root directory of this source tree.

import type { FunctionComponent, VNode } from 'preact';
import { Suspense as PreactSuspense, createElement as createElementBackground } from 'preact/compat';
import { Suspense as PreactSuspense } from 'preact/compat';
import { useRef } from 'preact/hooks';

import { createElement as createElementMainThread } from '@lynx-js/react/lepus';

import type { BackgroundSnapshotInstance } from '../backgroundSnapshot.js';
import { createElement } from '../createElement.js';
import { globalBackgroundSnapshotInstancesToRemove } from '../lifecycle/patch/commit.js';

export const Suspense: FunctionComponent<{ children: VNode | VNode[]; fallback: VNode }> = (
{ children, fallback },
) => {
const __createElement =
(__MAIN_THREAD__ ? createElementMainThread : createElementBackground) as typeof createElementBackground;
const childrenRef = useRef<BackgroundSnapshotInstance>();

// @ts-expect-error wrapper is a valid element type
const newChildren = __createElement('wrapper', {
const newChildren = createElement('wrapper', {
ref: (bsi: BackgroundSnapshotInstance) => {
if (bsi) {
childrenRef.current = bsi;
Expand All @@ -28,7 +25,7 @@ export const Suspense: FunctionComponent<{ children: VNode | VNode[]; fallback:
}, children);

// @ts-expect-error wrapper is a valid element type
const newFallback = __createElement('wrapper', {
const newFallback = createElement('wrapper', {
ref: (bsi: BackgroundSnapshotInstance) => {
if (bsi && childrenRef.current) {
const i = globalBackgroundSnapshotInstancesToRemove.indexOf(childrenRef.current.__id);
Expand All @@ -40,5 +37,5 @@ export const Suspense: FunctionComponent<{ children: VNode | VNode[]; fallback:
},
}, fallback);

return __createElement(PreactSuspense, { fallback: newFallback }, newChildren);
return createElement(PreactSuspense, { fallback: newFallback }, newChildren);
};
Loading