React: Is it possible to call a higher-order component within a container component?

1.1k views Asked by At

In my codebase I have a higher-order component (HOC) I use to add all the input validation functionality to a given component. It works great when used on a defined component like so...

let NameInput = React.createClass({
    render() {
        return (
            <div>
                <label htmlFor="name-input">Name</label>
                <input name="name-input" />
            </div>
        );
    }
});

let NameInput = addInputValidation(NameInput);

module.exports = NameInput;

But I now have a need to define a series of inputs based on an array from the server. Something like this...

let TestApp = React.createClass({
    render() {
        // Pretend the names array came from the server and is actually an array of objects.
        let names = ['First name', 'Middle name', 'Last name'];

        // Map over our names array in hopes of ending up with an array of input elements
        let nameComponents = names.map((name, index) => {
            let componentToRender = (
                    <div key={index}>
                        <label htmlFor={name}>{name}</label>
                        <input name={name} />
                    </div>
            );

            // Here is where I'd like to be able to use an HOC to wrap my name inputs with validation functions and stuff
            componentToRender = addInputValidation(componentToRender);

            return componentToRender;
        })


        return (
            <div>
                <p>Enter some names:</p>
                {nameComponents}
            </div>
        );
    }
})

let addInputValidation = function(Component) {
    let hoc = React.createClass({
        getInitialState() {
            return {
                isValid: false
            };
        },
        render() {
            return (
                <div>
                    <Component {...this.props} />
                    {this.state.isValid ? null : <p style={{color: "red"}}>Error!!!!</p>}
                </div>
            );
        }
    });

    return hoc;
}

module.exports = TestApp;

React doesn't like it when you try to render the result of calling an HOC from within another component.

I assume it has something to do with the fact that my componentToRender isn't really a React component or something.

So my questions are...

Why can't I call a HOC from within another component?

Is there a way to call a HOC on each element of an array?

Here's a jsfiddle that might help: https://jsfiddle.net/zt50r0wu/

EDIT TO CLARIFY SOME THINGS:

The array that I'm mapping over is actually an array of objects that describe the details of the input. Including the type of input (select, checkbox, text, etc).

Also my addInputValidation HOC actually takes more arguments than just the component. It takes an array of store indexes that will be pulled from the Redux store to be used for validation. These store indexes are derived from information in the array of objects describing the inputs. Having access to this potentially dynamic array is the reason I want to be able to call my HOC within the React life-cycle.

So mapping over my array of inputs might look more like this...

let Select = require('components/presentational-form/select');
let Text = require('components/presentational-form/select');
let CheckboxGroup = require('components/presentational-form/select');
let TestApp = React.createClass({
    render() {
        // Pretend the inputs array came from the server
        let inputs = [{...}, {...}, {...}];
        // Map over our inputs array in hopes of ending up with an array of input objects
        let inputComponents = inputs.map((input, index) => {    
            let componentToRender = '';

            if (input.type === 'select') {
                componentToRender = <Select labelText={input.label} options={input.options} />;
            } else if (input.type === 'text') {
                componentToRender = <Text labelText={input.label} />;
            } else if (input.type === 'checkbox') {
                componentToRender = <CheckboxGroup labelText={input.label} options={input.options} />;
            }

            // Here is where I'd like to be able to use an HOC to wrap my name inputs with validation functions and stuff
            componentToRender = addInputValidation(componentToRender, input.validationIndexes);

            return componentToRender;
        })


        return (
            <div>
                <p>Enter some names:</p>
                {inputComponents}
            </div>
        );
    }
})
3

There are 3 answers

0
Jeff McCloud On BEST ANSWER

The thing I think you're tripping over is the distinction between a component vs. an element. I find it helpful to think of a component as a function and an element as the result of that function. So all you're really trying to do is conditionally choose one of three different functions, pass it some arguments, and the display the results. I believe you want something like this:

(this can be cleaned up, BTW, just tried to preserve your code structure as much as possible)

let Select = require('components/presentational-form/select');
let Text = require('components/presentational-form/select');
let CheckboxGroup = require('components/presentational-form/select');
let TestApp = React.createClass({
    render() {
        // Pretend the inputs array came from the server
        let inputs = [{...}, {...}, {...}];
        // Map over our inputs array in hopes of ending up with an array of input objects
        let inputComponents = inputs.map((input, index) => {    
            let ComponentToRender;
            let props;            

            if (input.type === 'select') {
                ComponentToRender = addInputValidation(Select);
                props = { labelText: input.label, options: input.options };
            } else if (input.type === 'text') {
                ComponentToRender = addInputValidation(Text);
                props = { labelText: input.label };
            } else if (input.type === 'checkbox') {
                ComponentToRender = addInputValidation(CheckboxGroup);
                props = { labelText: input.label, options: input.options };
            }

            let element = <ComponentToRender {...props} />;

            return element;
        })


        return (
            <div>
                <p>Enter some names:</p>
                {inputComponents}
            </div>
        );
    }
})
2
Felix Kling On

Regarding your edit: The problem is still that you are returning a component instead of an element from the .map callback. But that can be easily solved by changing

return componentToRender;

to

return React.createElement(componentToRender);

The problems in your code are:

  • addInputValidation expects to be passed a component but you are passing it an element (<div />).
  • JSX expects to be passed an (array of) element(s) but you are passing it an array of components.

It seems the simplest way to solve your problem would be create a generic Input component that accepts the name and value as prop:

let Input = React.createClass({
    render() {
        return (
            <div>
                <label htmlFor={this.props.name}>{this.props.name}</label>
                <input name={this.props.name} />
            </div>
        );
    }
});

module.exports = addInputValidation(Input);

Which is used as

let nameComponents = names.map((name, index) => <Input key={index} name={name} />);
3
gaperton On

Yes, there is a way of course. But HOC is quite a painful way to handle validation in general.

There is a different approach to validation, based on ValueLink pattern.

Just compare the resulting code to higher order components approach.