Skip to main content
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

vnode
VNode
required
The virtual node to clone. Must be a valid VNode created with h() or createElement().
props
object
Additional props to merge with the original VNode’s props. Special props like key and ref are handled separately.
children
ComponentChildren
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, {});
}

Performance Considerations

  • 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

Build docs developers (and LLMs) love