Search code examples
reactjsecmascript-6refclone-element

pass ref to a class component with React.cloneElement and render prop


I'm writing a component that handle some internal state according to a ref of it's child (a mouse event related to that child's ref for example).
This component is using a render-prop to pass on the relevant piece of state to it's child, and render the child with the ref attached via React.cloneElement util.

The problem is that when the child is a class component, for some reason the ref is not available, and i can't find a way to render it as it's a react element object with a type of function (after i clone it of course).

But if the child is just a DOM node like a div for example, it is working as expected.

My work-around is to check the type of the child, and if it is a type of function I'll wrap the cloned element with my own div, if it's just a dom node then render as is.
However, i would like to not wrap the child with an extra div as i don't want to add unnecessary DOM nodes.

Here is a basic code example, most code removed for brevity:
The Parent component:

class Parent extends Component {

    attachRef = node => {
        this.ref = node;
    }

    render() {
        const { render } = this.props;
        const { someValue } = this.state;
        const Child = render(someValue);
        const WithRef = React.cloneElement(Child, {
            ref: this.attachRef
        });
        if (typeof WithRef.type === 'string') { // node element
            return WithRef;
        }
        else if (typeof WithRef.type === 'function') {
            // this is a react element object.. not sure how to render it
            // return ?
        } else {
            // need to find a way to render without a wrapping div
            return (
                <div ref={this.attachRef}>{Child}</div>
            );
        }
    }
}

The usage:

class App extends Component {
    render() {
        return (
            <div>
                <Parent render={someValue => <div> {someValue}</div>} />
                <Parent render={someValue => <Menu someValue={someValue} />} />
            </div>
        );
    }
}

When i render regular DOM nodes like the first example it works fine, when i try to render the Menu (which is a class component) it doesn't work as mentioned above.


Solution

  • I had almost an identical issue.
    i chose to use findDOMNode from react-dom, you can see the full solution in react-external-click.

    Although the warning notes:

    findDOMNode is an escape hatch used to access the underlying DOM node. In most cases, use of this escape hatch is discouraged because it pierces the component abstraction.

    findDOMNode only works on mounted components (that is, components that have been placed in the DOM). If you try to call this on a component that has not been mounted yet (like calling findDOMNode() in render() on a component that has yet to be created) an exception will be thrown.

    findDOMNode cannot be used on functional components.

    I think this is the better solution for this particular challenge.
    It let's you be "transparent" to the consumer, while being able to target the component in the DOM.

    Ok here it is, grabbing the ref:

    componentDidMount() {
        this.ref = findDOMNode(this);
        // some logic ...
    }
    

    this is how i use a render function with no wrapper of my own:

    render() {
            const { children, render } = this.props;
            const { clickedOutside } = this.state;
            const renderingFunc = render || children;
    
            if (typeof renderingFunc === 'function') {
                return renderingFunc(clickedOutside);
            } else {
                return null
            }
        }
    }