cloneElement

Creates a copy of an existing React element with extra or overridden props.

Signature
const clonedElement = cloneElement(element, props, ...children)

Parameters

ParameterTypeDescription
$elementReact elementA valid React element — a JSX node, the result of createElement, or another clone.
$propsobject | nullProps to shallow-merge over the original ones. Pass null to keep all original props. A `key` or `ref` here replaces the original.
$...childrenoptional...React.ReactNodeReplacement children. If you pass none, the original element's children are preserved.

Return value

React elementA new React element whose `type` matches the original and whose props are a shallow merge of the original props with the ones you pass. The original element is not modified.

Errors

Throws if the first argument is not a valid React element — for example a string, a number, an array, or null coming out of this.props.children.

Changelog

  • PHP 19.0Because `ref` is an ordinary prop for function components, passing `ref` in the props argument overrides it the same way any other prop does.

Cloning lets a parent add props to children it did not create — the classic tab list that tells each <Tab> whether it is active, or a form that stamps a shared disabled flag onto whatever was passed in.

When to reach for it

Only when you genuinely do not control the children and cannot restructure the API. React's own guidance treats this as a last resort, and the alternatives are almost always better:

  • Context — the parent provides the value, each child reads it. No cloning, works at any depth.
  • Render props — accept a function as children and call it with the data.
  • An array prop — take items={[...]} and render the children yourself.

What trips people up

The props you pass win over the caller's, so a consumer writing <Tab isActive={false} /> is silently overridden — data flow becomes impossible to trace from the child's own JSX. Cloning is shallow: nested props are shared, not copied. And children are not always elements. Text, numbers, null and booleans all appear in children, and passing any of them here throws, so guard with isValidElement or map with Children.map.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Mark one child in a tab strip or menu as the selected one
  • Stamp a shared disabled or size prop onto arbitrary form children
  • Give each child in a list its index without changing the child's API

Frequently asked questions

How do I pass props to this.props.children?

Map over the children with Children.map and clone each one with the extra props, guarding non-elements with isValidElement. Context or a render prop is usually a better fit, because cloning only reaches direct children.

Why does cloneElement say the element type is invalid?

You passed something that is not an element — most often a text node, a number, `null`, or an array from this.props.children. Filter with isValidElement before cloning.

Does cloneElement mutate the original element?

No. It returns a new element and leaves the original untouched, which is why you must use the return value rather than calling it for its effect.

More examples

Guarding against non-element children

Both inputs render disabled, and the stray whitespace string and `false` pass through untouched. Without the isValidElement guard, the whitespace child alone would make cloneElement throw.

Example · jsx
import { Children, cloneElement, isValidElement } from 'react';

function Field({ label, disabled }) {
  return (
    <p>
      {label}: <input disabled={disabled} defaultValue="text" />
    </p>
  );
}

function FieldSet({ disabled, children }) {
  return (
    <div>
      {Children.map(children, (child) =>
        // Strings, numbers, null and false all reach here
        isValidElement(child) ? cloneElement(child, { disabled }) : child
      )}
    </div>
  );
}

export default function App() {
  return (
    <FieldSet disabled={true}>
      <Field label="Name" />
      {'  '}
      {false}
      <Field label="Email" />
    </FieldSet>
  );
}

The context alternative

Same behaviour with no cloning. Each Tab reads the active index itself, so it keeps working when wrapped in a div, a Fragment, or another component — cases where cloning direct children silently stops reaching them.

Example · jsx
import { createContext, useContext, useState } from 'react';

const ActiveTabContext = createContext(0);
const TabIndexContext = createContext(0);

function Tab({ index, title }) {
  const active = useContext(ActiveTabContext);
  return <p>{index === active ? '▸ ' : '· '}{title}</p>;
}

function TabList({ children }) {
  const [active, setActive] = useState(0);
  return (
    <ActiveTabContext.Provider value={active}>
      {children}
      <button onClick={() => setActive((a) => (a + 1) % 3)}>Next</button>
    </ActiveTabContext.Provider>
  );
}

export default function App() {
  return (
    <TabList>
      <Tab index={0} title="Overview" />
      <Tab index={1} title="Pricing" />
      <Tab index={2} title="Support" />
    </TabList>
  );
}

Replacing children instead of merging props

The clone keeps className and data-id but shows 'replacement text'. The last line proves the source element was not mutated — cloning always produces a new object.

Example · jsx
import { cloneElement } from 'react';

const original = (
  <li className="row" data-id="42">
    original text
  </li>
);

export default function Demo() {
  // props: null keeps className and data-id; children are replaced
  const replaced = cloneElement(original, null, 'replacement text');

  return (
    <ul>
      {original}
      {replaced}
      <li>original untouched: {original.props.children}</li>
    </ul>
  );
}

Discussion

  • Be the first to comment on this lesson.