How to modify props of “root” element?

<aside> ➡️ TLDR: Solution for modifying props of root element is here

</aside>

“root” element, in this example, refers to the most parent element (i.e. the top level element) returned from a render method. If you intend to modify some of its children props, consider looking here.

Example: we have a component that renders a <button>, but we want our extension to change this component to show a disabled button and add event listener:

The original source code:

/** @namespace Component/CheckoutDeliveryOption/Component */
class CheckoutDeliveryOptionComponent extends React.PureComponent {
	render(): ReactElement {
	    return (
	        <button
	          block="Button"
					/>
	    );
	}
}

The intended modification:

/** @namespace Component/CheckoutDeliveryOption/Component */
class CheckoutDeliveryOptionComponent extends React.PureComponent {
	render(): ReactElement {
	    return (
	        <button
	          block="Button"
						disabled
						onMouseEnter={()=>alert('disabled')}
					/>
	    );
	}
}

Extension should not modify the original source code. This means we are forced to build a plugin here. We are working with a class, that has a namespace Component/CheckoutDeliveryOption/Component and we intend to modify the render method of it.

According to the plugin docs we should use memeber-method plugin for this case:

export default {
  'Component/CheckoutDeliveryOption/Component': {
		/* 
			 Not a good practice to define functions inside export, better to have it 
			 separate. This is for demonstration purposes only.
		*/
    'member-method': {
      render: () => (
				<button
          block="Button"
					disabled
					onMouseEnter={()=>alert('disabled')}
				/>
			)
    }
  }
}

This might work. But what if styles for a button change? Or, what if the content of a button change? We do not know what will the styling department do, or what other plugins will return, our extensions should work in all cases.

<aside> 💡 Ideally, we should consider the result of render function execution as a black box and operate on it assuming it returns a valid type, in our case ReactElement.

</aside>

Untitled

Here is how, we could use it:

export default {
  'Component/CheckoutDeliveryOption/Component': {
		/* 
			 Not a good practice to define functions inside export, better to have it 
			 separate. This is for demonstration purposes only.
		*/
    'member-method': {
      render: (args, callback) => (
				React.cloneElement(
					callback(...args),
					{ disabled, onMouseEnter: () => alert('disabled')}
				)
			)
    }
  }
}

But, this is not safe! Sometimes, the styling team changes the return type of a function and decides to return null. That is common, so let’s handle such a case and also let’s refactor the render method to a separate function for better readability and organization of code:

const addAlertToButton = (args, callback) => {
	// vvv Get original render
	const result = callback(...args);

  // vvv Check if element is null or non valid
	if (!React.isValidElement(result)) {
		// unexpected result returned, do nothing
		return result;
	}

  // vvv Modify element's props
	return React.cloneElement(
		result,
		{ disabled, onMouseEnter: () => alert('disabled') }
	);
}

export default {
  'Component/CheckoutDeliveryOption/Component': {
		/* 
			 Good practice to have descriptive function names for plugins. Plugins' 
			 names should describe what they're modyfing to the original function. 
		*/
    'member-method': {
      render: addAlertToButton
    }
  }
}

What are the real examples of this?

In this example, we use cloneElement to pass additional props (isLoadingSummary, removeStoreCredit, handleStoreCreditUpdate) to renderSummary function result.

Note: this example, does not check for isValidElementtherefore it could break.

How to modify props of “roots” child components?

<aside> ➡️ TLDR: Solution for modifying props of child element is here

</aside>

Example: We have a component that renders a <button> with a child <span>, say we want our extension to change span’s style to display: "block" and add event listener:

The original source code:

/** @namespace Component/CheckoutDeliveryOption/Component */
class CheckoutDeliveryOptionComponent extends React.PureComponent {
	render(): ReactElement {
	    return (
	        <button
	          block="Button"
					>
						<span>
						{ __('Ship here') }
						</span>
					</button>
	    );
	}
}

The intended modification:

/** @namespace Component/CheckoutDeliveryOption/Component */
class CheckoutDeliveryOptionComponent extends React.PureComponent {
	render(): ReactElement {
	    return (
	        <button
	          block="Button"
					>
						<span 
							style={{display: "block"}}
							onMouseEnter={()=>alert('block')}
						>
						{ __('Ship here') }
						</span>
					</button>
	    );
	}
}

Coming from previous knowledge, lets use a plugin and member-method to achieve this. From React.cloneElement docs , third parameter to cloneElement is children:

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

So lets modify the children array and render a modified span there:

const addAlertAndDisplayToSpan = (args, callback) => {
	const result = callback(...args);

	if (!React.isValidElement(result)) {
		// unexpected result returned, do nothing
		return result;
	}

	return React.cloneElement(
		result,
		result.props,
		[
			(
				<span 
					style={{display: "block"}}
					onMouseEnter={()=>alert('block')}
				>
				{ __('Ship here') }
				</span>
			)
		]
	);
}

export default {
  'Component/CheckoutDeliveryOption/Component': {
		/* 
			 Good practice to have descriptive function names for plugins. Plugins' 
			 names should describe what they're modyfing to the original function. 
		*/
    'member-function': {
      render: addAlertAndDisplayToSpan
    }
  }
}

But, is this a good solution? What if there were more children? What if the span was removed or altered by plugins or theme from original render altogether? How do we consider these cases? As we said in the first example:

Ideally, we should consider the result of render function execution as a black box and operate on it assuming it returns a valid type

But how does this fit with our specific case? Following the plugins’ philosophy, ideal goal is to try and keep everything as extensible and independent of each-other as possible, to use the least amount of assuming. Though, in our case, to modify props of a child element, at minimum we should either assume its type, or its location. Both can change and are equally fragile, this is why you should avoid nested rendering in your components as much as possible, but sometimes it is what it is.

Assuming that span (Or whatever the target element) will be rendered first in children, we can map rendered children, get the first child and modify its props using React.cloneElement :

/*
	DON'T COPY YET, THIS WILL NOT WORK, KEEP READING.
*/
const addAlertAndDisplayToSpan = (args, callback) => {
    const result = callback(...args);

    if (!React.isValidElement(result)) {
        // unexpected result returned, do nothing
        return result;
    }

    return React.cloneElement(
        result,
        result.props,
        // vvv Map children
        r*esult.props.children.map*(
            // vvv if child's index is 0, return it with modified props
            (child, i) => (i === 0 ? React.cloneElement(
                child,
                {
                    ...child.props,
                    style: { display: 'block' },
                    onMouseEnter: () => alert('block'),
                }
                // vvv otherwise, return the original child
            ) : child)
        )
    );
};

Looks good, But! Try to run this and you will get an error that result.props.children is not an array. This is true because react doesn’t create array for children if there is only one child as in our case. To manage this and other edge cases, we should use React.Children to map children:

const addAlertAndDisplayToSpan = (args, callback) => {
		// vvv Get original render
    const result = callback(...args);

		// vvv Check if element is null or non valid
    if (!React.isValidElement(result)) {
        // unexpected result returned, do nothing
        return result;
    }

		// vvv Clone the element, re-map its children
    return React.cloneElement(
        result,
        result.props,
        // vvv Map children
        React.Children.map(
            result.props.children,
            // vvv if child's index is 0, return it with modified props
            (child, i) => (i === 0 ? React.cloneElement(
                child,
                {
                    ...child.props,
                    style: { display: 'block' },
                    onMouseEnter: () => alert('block'),
                }
                // vvv otherwise, return original child
            ) : child)
        )
    );
};

What are the real examples of this?

TODO: Embed

TODO: Description


More articles on this topic

<aside> 💡 React.cloneElement documentation

</aside>

<aside> 💡 React.Children documentation

</aside>