Back to Blog
By AriesZhou · · 5 min read

cloneElement and Dynamic Component Rendering

react

Can you “inject” new props into an already rendered component after the fact? For example, if a table column component is already defined, can you dynamically add a ref or other attributes to it at render time?

In React, props are usually passed when a component is called. But in some cases, we need to dynamically modify or enhance elements that have already been created. This is where cloneElement comes in.

What is cloneElement

React.cloneElement is used to clone a React element and add or override its props.

React.cloneElement(element, props, ...children)

Basic syntax

const clonedElement = React.cloneElement(
  <ChildComponent title="原始标题" />,
  { title: "新标题", onClick: handleClick },
  <span>新的子元素</span>
);

The second argument is merged with the original element’s props, and the third argument overrides the original element’s children.

For example

import { cloneElement } from 'react';

const ButtonWrapper = ({ children, variant = 'primary' }) => {
  return cloneElement(children, {
    className: `btn btn-${variant} ${children.props.className || ''}`
  });
};

// 使用
<ButtonWrapper variant="danger">
  <button className="custom-class">点击</button>
</ButtonWrapper>

Rendered result: <button class="btn btn-danger custom-class">点击</button>

How it works

Internal implementation

The core logic of cloneElement is actually quite simple:

// 简化版本
function cloneElement(element, props, ...children) {
  return {
    ...element,
    props: {
      ...element.props,
      ...props,
      children: children.length > 0 ? children : element.props.children
    }
  };
}

It does three things:

  1. Copies the original element’s props
  2. Overrides existing props with new props
  3. Replaces existing children with new children

Key point

Props are merged, not replaced. Existing props are preserved unless a prop with the same name in the new props overrides them.

const original = <Input type="text" placeholder="原始" disabled />;

const cloned = cloneElement(original, { placeholder: "新提示", required: true });
// 结果: { type: "text", placeholder: "新提示", disabled: true, required: true }

Common use cases

Dynamically injecting a ref

This is the most typical use case: a parent component needs to access a child component’s ref.

import { cloneElement, forwardRef } from 'react';

const EditorWrapper = forwardRef(({ children, onSave }, ref) => {
  const internalRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => internalRef.current?.focus(),
    getValue: () => internalRef.current?.value
  }));

  // 给子组件注入 ref
  const childWithRef = cloneElement(children, {
    ref: internalRef
  });

  return (
    <div>
      {childWithRef}
      <button onClick={onSave}>保存</button>
    </div>
  );
});

3.2 Prop injection

A parent component dynamically adds props to a child component:

const DataFetcher = ({ children, url }) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url).then(setData);
  }, [url]);

  // 给 children 注入 data prop
  return cloneElement(children, { data });
};

// 使用
<DataFetcher url="/api/users">
  <UserList />
</DataFetcher>

3.3 HOC enhancement

Using cloneElement to implement a simple HOC:

const withLogger = (Component) => {
  return function LoggedComponent(props) {
    const handleClick = (e) => {
      console.log('clicked:', e.target);
      props.onClick?.(e);
    };

    return cloneElement(<Component {...props} />, { onClick: handleClick });
  };
};

Combined use: cloneElement + forwardRef

This is a very powerful combination in practice.

Why the combination is needed

Using cloneElement or forwardRef alone has limitations:

  • cloneElement: can add props, but cannot pass a ref
  • forwardRef: can pass a ref, but cannot inject props dynamically

Only by combining the two can you achieve “both passing a ref and injecting props”:

const DynamicEditor = forwardRef(({ component: EditorComponent, editorProps }, ref) => {
  // 校验是否是有效的 React 元素
  if (!isValidElement(EditorComponent)) {
    return null;
  }

  // 组合:既有 ref,又有额外 props
  const element = cloneElement(EditorComponent, {
    ...editorProps,
    ref
  });

  return element;
});

When using it

import { cloneElement, forwardRef, isValidElement, useRef } from 'react';

// 中间层组件:负责渲染具体编辑器
const EditorRenderer = forwardRef(({ component: Editor, editorProps }, ref) => {
  if (!isValidElement(Editor)) {
    return null;
  }

  // 给具体编辑器注入 ref 和 props
  const element = cloneElement(Editor, {
    ...editorProps,
    ref
  });

  return element;
});

// 具体编辑器:实现特定输入逻辑
const TextEditor = forwardRef(({ value, onChange, onBlur }, ref) => {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    getValue: () => inputRef.current?.value
  }), []);

  return (
    <input
      ref={inputRef}
      value={value}
      onChange={(e) => onChange(e.target.value)}
      onBlur={onBlur}
    />
  );
});

// 使用
const Parent = () => {
  const editorRef = useRef(null);

  const handleSave = () => {
    console.log(editorRef.current?.getValue());
  };

  return (
    <EditorRenderer
      component={TextEditor}
      editorProps={{
        value: 'hello',
        onChange: console.log,
        onBlur: handleSave
      }}
    />
  );
};

Defensive programming: isValidElement

Before using cloneElement, it is best to validate first:

import { isValidElement } from 'react';

// 校验元素是否有效
if (!isValidElement(someElement)) {
  return null; // 或者抛出错误
}

const cloned = cloneElement(someElement, { newProp: 'value' });

Why validation is needed

  • Avoid crashes caused by passing null or undefined as a component
  • Prevent passing non-React elements (such as strings or numbers)
  • Make error messages clearer

Simplified form

const SafeClone = ({ children, ...props }) => {
  if (!isValidElement(children)) {
    return children;
  }

  return cloneElement(children, props);
};

Comparison with other approaches

Render Props

// render props
<DataFetcher render={(data) => <Child data={data} />} />

// cloneElement
<DataFetcher>
  <Child />
</DataFetcher>

cloneElement is more intuitive and does not require changing how the component is used.

Context

// Context
<ThemeProvider>
  <Child />
</ThemeProvider>

// cloneElement
<Parent>
  <Child theme="dark" />
</Parent>

cloneElement is suitable for one-time injection, while Context is suitable for global sharing.

Summary

FunctionPurpose
cloneElementClone and modify a React element
forwardRefLet a ref pass through a component
isValidElementCheck whether something is a valid React element

A typical pattern combining all three:

const Renderer = forwardRef(({ component, props }, ref) => {
  if (!isValidElement(component)) return null;

  return cloneElement(component, { ...props, ref });
});

The essence of this pattern is: combining ref passing with prop injection, so that a parent component can both call child component methods and dynamically control child component behavior.

References