ReactJS

React: Apply Props Recursively

Providing this was necessary during some painful email developments because:

  • some styles only work inline
  • some styles only work inline on immediate element

This function is to be used as such:

// inside a component, grabbing `children` from the props:

<div>
  {applyRecursiveProps(children, { style: {color: 'blue'} })}
</div>

applyRecursiveProps.js

The applyRecursiveProps service is as follows:

import React from 'react'

/**
 * Assign properties to react `children`, and all their recursive `children`.
 */

export default function applyRecursiveProps(children, newProps = {}) {
    if (typeof children === 'undefined') return // has no children elements

    return React.Children.map(children, (child) => {
        if (!child.props) return child // is text node, without a tag
        const style = { ...child.props.style, ...newProps.style } // to avoid all the style objects merging together into one
        const recursiveGrandchildren = applyRecursiveProps(child.props.children, newProps)
        const childNewProps = { children: recursiveGrandchildren, ...newProps, style }

        return React.cloneElement(child, childNewProps)
    })
}

applyRecursiveProps.test.js

And for bonus, here are the tests for it:

import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render } from '@testing-library/react'
import applyRecursiveProps from '../applyRecursiveProps'

const TestElement = ({ children, ...props }) => (
    <div {...props}>{applyRecursiveProps(children, { testprop: 'test value' })}</div>
)

test('with one level of elements, new props are applied', () => {
    const { getByTestId } = render(
        <TestElement>
            <p data-testid="p1">1</p>
            <p data-testid="p2">2</p>
        </TestElement>,
    )

    expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
    expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
})

test('with two level of elements, new props are applied', () => {
    const { getByTestId } = render(
        <TestElement>
            <p data-testid="p1">
                <span data-testid="span">span1</span>
            </p>
            <p data-testid="p2">2</p>
        </TestElement>,
    )

    expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
    expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
    expect(getByTestId('span')).toHaveAttribute('testprop', 'test value')
})

test('with three level of elements, new props are applied', () => {
    const { getByTestId } = render(
        <TestElement>
            <p data-testid="p1">
                <span data-testid="span1">
                    <span data-testid="span2">span2</span>
                </span>
            </p>
            <p data-testid="p2">2</p>
        </TestElement>,
    )

    expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
    expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
    expect(getByTestId('span1')).toHaveAttribute('testprop', 'test value')
    expect(getByTestId('span2')).toHaveAttribute('testprop', 'test value')
})

test('styles object is not mixed when passed onto the next level', () => {
    const { getByTestId } = render(
        <TestElement>
            <p data-testid="p1" style={{ display: 'none' }}>
                <span data-testid="span1">
                    <span data-testid="span2">span2</span>
                </span>
            </p>
            <p data-testid="p2">2</p>
        </TestElement>,
    )

    expect(getByTestId('p1')).toHaveStyle('display: none')
    expect(getByTestId('p2')).not.toHaveStyle('display: none')
    expect(getByTestId('span1')).not.toHaveStyle('display: none')
    expect(getByTestId('span2')).not.toHaveStyle('display: none')
})
Standard

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.