V1.0

recursiveCloneChildren

Recursively clones children, adding additional props to components with matched display names.

Installation

Create a utils/recursive-clone-children.tsx file and paste the following code into it.

/utils/recursive-clone-children.tsx
import * as React from 'react';
 
/**
 * Recursively clones React children, adding additional props to components with matched display names.
 *
 * @param children - The node(s) to be cloned.
 * @param additionalProps - The props to add to the matched components.
 * @param displayNames - An array of display names to match components against.
 * @param uniqueId - A unique ID prefix from the parent component to generate stable keys.
 * @param asChild - Indicates whether the parent component uses the Slot component.
 *
 * @returns The cloned node(s) with the additional props applied to the matched components.
 */
export function recursiveCloneChildren(
  children: React.ReactNode,
  additionalProps: any,
  displayNames: string[],
  uniqueId: string,
  asChild?: boolean,
): React.ReactNode | React.ReactNode[] {
  const mappedChildren = React.Children.map(
    children,
    (child: React.ReactNode, index) => {
      if (!React.isValidElement(child)) {
        return child;
      }
 
      const displayName =
        (child.type as React.ComponentType)?.displayName || '';
      const newProps = displayNames.includes(displayName)
        ? additionalProps
        : {};
 
      const childProps = (child as React.ReactElement<any>).props;
 
      return React.cloneElement(
        child,
        { ...newProps, key: `${uniqueId}-${index}` },
        recursiveCloneChildren(
          childProps?.children,
          additionalProps,
          displayNames,
          uniqueId,
          childProps?.asChild,
        ),
      );
    },
  );
 
  return asChild ? mappedChildren?.[0] : mappedChildren;
}

Examples

Without asChild

If the component doesn't use Slot from @radix-ui/react-slot, you don't need to worry about the last parameter of the recursiveCloneChildren function.

Here is an example without asChild:

/demo-recursive-clone-children.tsx
import * as React from 'react';
import { cnExt } from '@/utils/cn';
import { recursiveCloneChildren } from '@/utils/recursive-clone-children';
 
const BOX_ROOT_NAME = 'BoxRoot';
const BOX_ICON_NAME = 'BoxIcon';
 
type BoxProps = {
  size?: 'large' | 'medium';
} & React.HTMLAttributes<HTMLDivElement>;
 
type SharedProps = Pick<BoxProps, 'size'>;
 
function Box({ children, className, size = 'large', ...rest }: BoxProps) {
  const uniqueId = React.useId();
  const sharedProps: SharedProps = {
    size,
  };
 
  const extendedChildren = recursiveCloneChildren(
    children as React.ReactElement[],
    sharedProps,
    [BOX_ICON_NAME],
    uniqueId,
  );
 
  return (
    <div
      className={cnExt(
        {
          'px-4 py-3': size === 'large',
          'px-3 py-2': size === 'medium',
        },
        className,
      )}
      {...rest}
    >
      {extendedChildren}
    </div>
  );
}
Box.displayName = BOX_ROOT_NAME;
 
function BoxIcon({
  size,
  className,
  ...rest
}: SharedProps & React.HTMLAttributes<HTMLDivElement>) {
  return (
    <RiInformationLine
      className={cn(
        {
          'size-5': size === 'large',
          'size-4': size === 'medium',
        },
        className,
      )}
      {...rest}
    />
  );
}
BoxIcon.displayName = BOX_ICON_NAME;
 
export { Box as Root, BoxIcon as Icon };

Here is how to use Box component:

/demo-recursive-clone-children.tsx
import * as Box from '@/components/box';
 
export function BoxExample() {
  return (
    <Box.Root size='medium'>
      Some content
      <Box.Icon />{' '}
      {/* size prop will be passed down to the Icon from the Root */}
    </Box.Root>
  );
}

With asChild

Let's consider the example above with asChild.

Here are the lines to add:

/demo-recursive-clone-children.tsx
import { Slot } from '@radix-ui/react-slot';
 
// ...
 
type BoxProps = {
  size?: 'large' | 'medium';
  asChild?: boolean;
} & React.HTMLAttributes<HTMLDivElement>;
 
// ...
 
function Box({
  // ...
  asChild,
  ...rest
}: BoxProps) {
  const uniqueId = React.useId();
  const Component = asChild ? Slot : 'div';
 
  // ...
 
  const extendedChildren = recursiveCloneChildren(
    children as React.ReactElement[],
    sharedProps,
    [BOX_ICON_NAME],
    uniqueId,
    asChild,
  );
 
  return (
    <Component
    // ...
    >
      {extendedChildren}
    </Component>
  );
}
// ...
© 2024 AlignUI Design System. All rights reserved.