Реакция: можно ли вызвать компонент более высокого порядка внутри компонента-контейнера?

В моей кодовой базе есть компонент высшего порядка (HOC), который я использую для добавления всех функций проверки ввода к данному компоненту. Он отлично работает при использовании на определенном компоненте, например...

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;

Но теперь мне нужно определить серию входных данных на основе массива с сервера. Что-то вроде этого...

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 не любит, когда вы пытаетесь отобразить результат вызова HOC из другого компонента.

Я предполагаю, что это как-то связано с тем фактом, что мой componentToRender на самом деле не является компонентом React или чем-то еще.

Итак, мои вопросы...

Почему я не могу вызвать HOC из другого компонента?

Есть ли способ вызвать HOC для каждого элемента массива?

Вот jsfiddle, который может помочь: https://jsfiddle.net/zt50r0wu/

РЕДАКТИРОВАТЬ, ЧТОБЫ УТОЧНИТЬ НЕКОТОРЫЕ ВЕЩИ:

Массив, который я отображаю, на самом деле представляет собой массив объектов, описывающих детали ввода. Включая тип ввода (выбор, флажок, текст и т. д.).

Кроме того, мой addInputValidation HOC на самом деле принимает больше аргументов, чем просто компонент. Требуется массив индексов хранилища, который будет извлечен из хранилища Redux для использования для проверки. Эти индексы хранилища получены из информации в массиве объектов, описывающих входы. Наличие доступа к этому потенциально динамическому массиву является причиной, по которой я хочу иметь возможность вызывать свой HOC в жизненном цикле React.

Таким образом, сопоставление моего массива входных данных может выглядеть примерно так...

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>
        );
    }
})

person Kory    schedule 04.01.2017    source источник
comment
addInputValidation ожидает передачи компонента, но вы передаете ему элемент (<div />). Это не может работать. Есть ли способ вызвать HOC для каждого элемента массива? Конечно. HOC — это функция, и очень просто вызвать функцию для каждого элемента массива. Работает это или нет, зависит от того, что представляют собой элементы и что ожидает функция. Если у вас есть массив components, он будет работать нормально.   -  person Felix Kling    schedule 05.01.2017
comment
@Kory, можете ли вы показать нам свой HOC addInputValidation?   -  person RazorHead    schedule 09.11.2017


Ответы (3)


Я думаю, что вы спотыкаетесь о различие между компонентом и элементом. Я считаю полезным думать о компоненте как о функции, а об элементе как о результате этой функции. Итак, все, что вы на самом деле пытаетесь сделать, это условно выбрать одну из трех различных функций, передать ей несколько аргументов и отобразить результаты. Я считаю, что вы хотите что-то вроде этого:

(это можно почистить, кстати, просто попытался максимально сохранить структуру кода)

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>
        );
    }
})
person Jeff McCloud    schedule 04.01.2017

Что касается вашего редактирования: проблема по-прежнему заключается в том, что вы возвращаете компонент вместо элемента из обратного вызова .map. Но это легко решается изменением

return componentToRender;

to

return React.createElement(componentToRender);

Проблемы в вашем коде:

  • addInputValidation ожидает передачи компонента, но вы передаете ему элемент (<div />).
  • JSX ожидает передачи (массива) элементов, но вы передаете ему массив компонентов.

Кажется, самым простым способом решить вашу проблему было бы создание общего компонента Input, который принимает имя и значение в качестве реквизита:

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);

Который используется как

let nameComponents = names.map((name, index) => <Input key={index} name={name} />);
person Felix Kling    schedule 04.01.2017
comment
Спасибо за быстрый ответ! Это прояснило некоторые вещи. Имеет смысл, что функция, ожидающая возврата компонента на основе переданного ей компонента, сломается, если вместо нее будет передан элемент. Моя основная проблема до сих пор не решена. Нет твоей вины. Думаю, я упростил некоторые вещи. Я добавлю редактирование, чтобы прояснить некоторые из них. В идеале было бы иметь возможность определить компонент React внутри другого компонента. У меня такое чувство, что это либо невозможно, либо антипаттерн. - person Kory; 05.01.2017
comment
Что касается вашего редактирования, все, что вам нужно сделать, чтобы вернуть элемент вместо компонента, это return React.createElement(componentToRender);. - person Felix Kling; 05.01.2017

Да, есть способ, конечно. Но HOC — довольно болезненный способ справиться с валидацией в целом.

Существует другой подход к проверка на основе Шаблон ValueLink.

Просто сравните полученный код с подходом компонентов более высокого порядка.

person gaperton    schedule 05.01.2017
comment
Мне нравится эта идея, но, вероятно, я не буду ее реализовывать в данный момент. Ваши статьи на Medium прекрасно это объясняют. Используете ли вы Redux/Flux в своих приложениях React? Если да, считаете ли вы, что функции Redux и ValueLink пересекаются или конфликтуют вообще? - person Kory; 05.01.2017
comment
Я вообще не использую Redux/Flux. Простые случаи (которые на самом деле могут быть довольно сложными страницами) могут быть прекрасно обработаны только с помощью ссылок, как показано здесь, и это выглядит намного лучше. medium.com/@gaperton/ Для крупных SPA мы используем github.com/Volicon/NestedReact — универсальное решение для управления состоянием, интегрированное с Links. Внутри есть несколько примеров. И я должен добавить, что я автор всего этого материала. - person gaperton; 05.01.2017
comment
Да, и популярное решение для управления состоянием, которое далеко от Flux и чем-то близко к NestedReact, — это mobx. Однако у него нет возможностей двусторонней привязки данных и сериализации. Но популярен. - person gaperton; 05.01.2017