7

Imagine having React component

function List() {
   return (<ul>
             <li>1</li>
             <li>2</li>
           </ul>
    );
}

I would like to create high-order component which modifies, for example, styles of all li node.

function makeRed(component) {
    return function(props) {
         const element = React.createElement(component, props);

         return React.cloneElement(
            element,
            element.props,
            React.Children.map(
                element.props.children,
                ch => React.cloneElement(ch, { 
                        ...ch.props, 
                        style: {
                           backgroundColor: "red"
                        }
                    },                    
                    ch.props.children
                )
            )
        );
    }
}

But. This doesn't work. Children are empty.

Interesting that this works if I create component directly, like

...    
const element = <ul><li>1</li><li>2</li></ul>;
...

Question: how to access a children and grandchildren of any React element?

STO
  • 10,390
  • 8
  • 32
  • 32
  • 9
    This very much seems like an anti-pattern in React. Why not instead create a `List` component that can accept styles as a prop, and a higher-order component that passes said prop to them? – Hamms Aug 09 '18 at 22:28
  • 2
    can you give a more practical example of what you want to do? your example is much more easily achieved with plain old css – azium Aug 09 '18 at 22:46
  • I need this to add "sortable (draggable)" behavior to any element with children with HOC. I know this is not a pure react way for normal components but that's just the simplest example illustrates problem. – STO Aug 10 '18 at 08:22
  • @STO this can be done as Hamms already said. – Jordan Enev Aug 27 '18 at 08:10

1 Answers1

4

This is an anti-pattern, as @hamms has pointed out. There are better ways to implement themes in React using plain old CSS.

Said that, here's a hack to a working example of your use case - https://codesandbox.io/s/ymqwyww22z.

Basically, here's what I have done:

  1. Make List a class based component. It's not too much trouble to wrap a functional component into one.

    import React, { Component } from "react";
    
    export default class List extends Component {
      render() {
        return (
          <ul>
            <li>1</li>
            <li>2</li>
          </ul>
        );
      }
    }
    
  2. Implement render in the dynamic class Red<Component> to first fetch the element-tree returned from the base Component's render and then edit it.

    import React from "react";
    
    export default function makeRed(Component) {
      return class RedComponent extends Component {
        constructor(props) {
          super(props);
    
          RedComponent.displayName = `Red${Component.name}`;
        }
    
        render() {
          let componentElement = super.render();
          let { children, ...rest } = componentElement.props;
          children = React.Children.map(children, child => {
            return React.cloneElement(child, {
              ...child.props,
              style: {
                backgroundColor: "red"
              }
            });
          });
          return React.cloneElement(componentElement, rest, ...children);
        }
      };
    }
    

How's this different from the createElement version of makeRed?

As makeRed returns a HOC, when you use it in your App component, you don't assign props to it. Something like this...

function App() {
  return <RedList />; // no props!
}

So inside the dynamic component function, where you use createElement to create a new instance, component.props don't carry any children. Since List creates its own children, you need to grab those and modify them, instead of reading children from props.

hazardous
  • 10,627
  • 2
  • 40
  • 52
  • 1
    Thanks for your answer, marked as accepted. So children becomes available only for inheriting components? If I call `render()` directly or call a functional component, would it be the same? – STO Aug 28 '18 at 19:10
  • With a functional component, you can directly call it to get the element tree, instead of `super.render`. You will need to make changes to the hoc to not extend the class in case Component is a function. – hazardous Aug 29 '18 at 01:33