Clones a virtual node (VNode) and optionally merges new props or replaces its children.
Signature
function cloneElement(
vnode: VNode<any>,
props?: any,
...children: ComponentChildren[]
): VNode<any>
function cloneElement<P>(
vnode: VNode<P>,
props?: any,
...children: ComponentChildren[]
): VNode<P>
Parameters
The virtual node to clone. Must be a valid VNode created with h() or createElement().
Additional props to merge with the original VNode’s props. Special props like key and ref are handled separately.
New children to replace the original VNode’s children. If provided, these replace (not merge with) the original children.
Return Value
Returns a new VNode with:
- Same
type as the original
- Merged props (original + new)
- New children if provided, otherwise original children
- New
key if provided, otherwise original key
- New
ref if provided, otherwise original ref
Description
cloneElement creates a shallow copy of a virtual node, allowing you to modify its props and children. This is useful for:
- Wrapping or modifying elements passed as children
- Adding props to elements dynamically
- Implementing higher-order components
- Extending component functionality
The function preserves the original VNode’s type while merging new props and optionally replacing children.
Implementation
The function is implemented in src/clone-element.js:14:
export function cloneElement(vnode, props, children) {
let normalizedProps = assign({}, vnode.props),
key,
ref,
i;
for (i in props) {
if (i == 'key') key = props[i];
else if (i == 'ref' && typeof vnode.type != 'function') ref = props[i];
else normalizedProps[i] = props[i];
}
if (arguments.length > 2) {
normalizedProps.children =
arguments.length > 3 ? slice.call(arguments, 2) : children;
}
return createVNode(
vnode.type,
normalizedProps,
key || vnode.key,
ref || vnode.ref,
NULL
);
}
Usage Examples
Basic Cloning
import { h, cloneElement } from 'preact';
const original = <div className="original">Hello</div>;
const cloned = cloneElement(original, {
className: 'cloned',
id: 'my-div'
});
// cloned:
// <div className="cloned" id="my-div">Hello</div>
Adding Props to Children
import { cloneElement } from 'preact';
function ParentComponent({ children }) {
// Add onClick handler to child
return cloneElement(children, {
onClick: () => console.log('Clicked!')
});
}
// Usage:
<ParentComponent>
<button>Click me</button>
</ParentComponent>
// Renders:
// <button onClick={...}>Click me</button>
Replacing Children
import { h, cloneElement } from 'preact';
const original = <div>Original children</div>;
const withNewChildren = cloneElement(
original,
null,
'New children'
);
// withNewChildren:
// <div>New children</div>
Merging Props
import { h, cloneElement } from 'preact';
function withExtraProps(element) {
return cloneElement(element, {
className: `${element.props.className || ''} extra`,
'data-enhanced': true
});
}
const original = <div className="base">Content</div>;
const enhanced = withExtraProps(original);
// enhanced:
// <div className="base extra" data-enhanced={true}>Content</div>
Extending Component Props
import { cloneElement } from 'preact';
function Button({ children, onClick }) {
return <button onClick={onClick}>{children}</button>;
}
function TrackedButton({ children }) {
const handleClick = (e) => {
console.log('Button clicked');
// Call original onClick if it exists
children.props.onClick?.(e);
};
return cloneElement(children, { onClick: handleClick });
}
// Usage:
<TrackedButton>
<Button onClick={() => alert('Original')}>Click me</Button>
</TrackedButton>
Working with React.Children Pattern
import { cloneElement, toChildArray } from 'preact';
function List({ children }) {
return (
<ul>
{toChildArray(children).map((child, index) =>
cloneElement(child, {
key: index,
className: `list-item ${child.props.className || ''}`,
'data-index': index
})
)}
</ul>
);
}
// Usage:
<List>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</List>
// Renders:
// <ul>
// <li key="0" className="list-item" data-index="0">Item 1</li>
// <li key="1" className="list-item" data-index="1">Item 2</li>
// <li key="2" className="list-item" data-index="2">Item 3</li>
// </ul>
Adding Event Handlers
import { cloneElement } from 'preact';
function ClickLogger({ children }) {
const logClick = (e) => {
console.log('Clicked:', e.target);
// Call original handler if exists
children.props.onClick?.(e);
};
return cloneElement(children, { onClick: logClick });
}
// Usage:
<ClickLogger>
<button onClick={() => alert('Hello')}>Click me</button>
</ClickLogger>
Updating Keys
import { cloneElement } from 'preact';
function KeyedList({ items, renderItem }) {
return items.map(item => {
const element = renderItem(item);
return cloneElement(element, { key: item.id });
});
}
// Usage:
const items = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' }
];
<KeyedList
items={items}
renderItem={item => <div>{item.name}</div>}
/>
Modifying Refs
import { createRef, cloneElement } from 'preact';
function RefWrapper({ children }) {
const myRef = createRef();
const handleMount = () => {
console.log('Element:', myRef.current);
};
useEffect(handleMount, []);
// Replace child's ref with our own
return cloneElement(children, { ref: myRef });
}
// Usage:
<RefWrapper>
<input type="text" />
</RefWrapper>
Higher-Order Component Pattern
import { cloneElement } from 'preact';
import { useState } from 'preact/hooks';
function withToggle(Component) {
return function ToggleWrapper(props) {
const [isOpen, setIsOpen] = useState(false);
return cloneElement(Component, {
...props,
isOpen,
toggle: () => setIsOpen(!isOpen)
});
};
}
function Dropdown({ isOpen, toggle, items }) {
return (
<div>
<button onClick={toggle}>Toggle</button>
{isOpen && <ul>{items.map(item => <li>{item}</li>)}</ul>}
</div>
);
}
const EnhancedDropdown = withToggle(<Dropdown items={['A', 'B', 'C']} />);
Conditionally Cloning
import { cloneElement } from 'preact';
function ConditionalWrapper({ condition, wrapper, children }) {
if (condition) {
return cloneElement(wrapper, null, children);
}
return children;
}
// Usage:
<ConditionalWrapper
condition={isHighlighted}
wrapper={<div className="highlight" />}
>
<p>This might be highlighted</p>
</ConditionalWrapper>
With TypeScript
import { cloneElement, VNode } from 'preact';
interface ButtonProps {
onClick: () => void;
disabled?: boolean;
}
function enhanceButton(button: VNode<ButtonProps>): VNode<ButtonProps> {
return cloneElement(button, {
className: 'enhanced-button',
onClick: () => {
console.log('Enhanced click');
button.props.onClick?.();
}
});
}
Important Behaviors
Props Merging
New props are merged with original props, not replaced:
const original = <div className="foo" id="bar">Text</div>;
const cloned = cloneElement(original, { className: 'baz' });
// cloned props: { className: 'baz', id: 'bar' }
// className is overwritten, id is preserved
Children Replacement
Children are replaced, not merged:
const original = <div>Original</div>;
const cloned = cloneElement(original, null, 'New');
// cloned: <div>New</div>
// "Original" is completely replaced
Key and Ref Handling
key and ref are special props:
const original = <div key="old" ref={oldRef}>Text</div>;
const cloned = cloneElement(original, { key: 'new', ref: newRef });
// cloned has key="new" and ref={newRef}
// For function components, ref is passed as a prop
Shallow Clone
cloneElement performs a shallow clone:
const original = <div data={{ nested: 'value' }}>Text</div>;
const cloned = cloneElement(original, {});
// cloned.props.data === original.props.data (same reference)
// Modifying cloned.props.data.nested affects original
Common Pitfalls
Not Preserving Original Handlers
// ❌ Overwrites original onClick
cloneElement(child, {
onClick: () => console.log('New handler')
});
// ✅ Preserves original onClick
cloneElement(child, {
onClick: (e) => {
child.props.onClick?.(e);
console.log('Additional handler');
}
});
Cloning Non-VNodes
// ❌ Doesn't work with strings, numbers, or null
cloneElement('text', {}); // Error
cloneElement(null, {}); // Error
// ✅ Only clone VNodes
if (typeof child === 'object' && child.type) {
cloneElement(child, {});
}
cloneElement creates a new VNode object
- Original VNode is not modified (immutable)
- Minimal overhead - only copies necessary properties
- Use sparingly in performance-critical paths
h - Create virtual nodes
createElement - React-compatible node creation
toChildArray - Convert children to array for mapping
isValidElement - Check if value is a VNode