是否在React State中存储JSX元素?

时间:2019-03-11 10:22:51

标签: javascript reactjs

我希望我不是唯一一个处理大量数据以创建JSX元素的人。我的解决方案中的问题是,每次状态或道具在组件内部发生更改时,它都会不必要地执行整个处理过程以创建其子项。

这是我的组件:

import React from "react";
import PropTypes from "prop-types";
import Input from "./Input";
import { validate } from "./validations";
class Form extends React.Component {

    static propTypes = {
        //inputs: array of objects which contains the properties of the inputs
        //Required.
        inputs: PropTypes.arrayOf(
            PropTypes.shape({
                //id: identifier of the input.
                //Required because the form will return an object where
                //the ids will show which value comes from which input.
                id: PropTypes.string.isRequired,

                //required: if set to false, this field will be accepted empty.
                //Initially true, so the field needs to be filled.
                //Not required.
                required: PropTypes.bool,

                //type: type of the input.
                //Initially text.
                //Not required.
                type: PropTypes.string,

                //tag: htmlTag
                //Initially "input", but can be "select".
                //Not required.
                tag: PropTypes.oneOf(["input", "select", "radio", "custom-select"]),

                //options: options for <select>
                //Not required.
                options: PropTypes.arrayOf(
                    PropTypes.oneOfType([
                        PropTypes.shape({
                            //value: value of the option
                            value: PropTypes.string.isRequired,
                            //displayValue: if defined it
                            //will be the displayed of the text
                            displayValue: PropTypes.string,
                            //element: for tag: custom-select
                            //must be a JSX element
                            element: PropTypes.object
                        }),
                        //if the value is equal to the display value,
                        //it can be declared as string
                        PropTypes.string
                    ])
                ),

                //minLen: minimum length accepted for the field.
                //If the input doesn't passes, it will not be valid.
                //Initially 0, not required.
                minLen: PropTypes.number,

                //maxLen: maximum length accepted for the field.
                //The characters over the maximum will be cut.
                //Initially 20000, not required.
                maxLen: PropTypes.number,

                //className: class of the container of the input.
                //The structure of the input component is:
                //<div className={className}>
                    //[<label>{label}</label>]
                    //<input>
                    //<p>{errorMessage}</p>
                //</div>
                //Not required.
                className: PropTypes.string,

                //placelholder: placeholder of the input field.
                //Not required.
                placeholder: PropTypes.string,

                //label: label of the input field.
                //Not required.
                label: PropTypes.string,

                //validation: function, which checks
                //if the value entered is valid.
                //Must return a string of an error message if isn't valid.
                //Executes if:
                //-the user clicks outside the field if it has focus
                //-the user clicks on submit button
                //Not required.
                validation: PropTypes.func,

                //process: function, which processes the input entered
                //If the form is ready to submit,
                //the field's value will be processed with it.
                //Not required.
                process: PropTypes.func,

                //initValue: initial value of the field.
                //Not required.
                initValue: PropTypes.string,

                //submitOnEnter: if the user presses the "Enter" key,
                //it submits the form.
                //works only with "input" tags
                //Initially true, not required.
                submitOnEnter: PropTypes.bool
            })
        ).isRequired,

        //onSubmit: function which processes the form.
        //Must receive the form as a parameter, which is the shape of:
        //{[id]: value, [id]: value}
        //Required.
        onSubmit: PropTypes.func.isRequired,

        //otherElements: addictional elements to the form.
        //The function must return JSX elements
        //Not required.
        otherElements: PropTypes.arrayOf(PropTypes.func),

        //className: className of the form.
        //Not required.
        className: PropTypes.string,

        //buttonTitle: the button's title.
        //Initially "Submit".
        //Not required.
        buttonText: PropTypes.string,

        //focusOn: puts focus on specified element on specified event.
        //Not required.
        focusOn: PropTypes.shape({
            id: PropTypes.string.isRequired,
            event: PropTypes.bool
        }),

        //collect: collects the specified element id's into one parent.
        //Needs to be an object, where the key is the classname of the container,
        //and the values are an array of the id's of the items needs to collect
        //Not required.
        collect: PropTypes.objectOf(PropTypes.array)
    }

    constructor (props) {
        super(props);

        this.state = {};
    }

    componentDidMount () {
        const { inputs } = this.props;

        const inputProps = inputs.reduce((obj, {id, initValue: value = ""}) => {
            obj[id] = { value, error: null};
            return obj;
        }, {});

        this.setState({...inputProps}) //eslint-disable-line
    }

    //process with max-length checking
    completeProcess = (val, process, maxLen) => process(val).substr(0, maxLen)

    handleSubmit = () => {
        const inputProps = this.state;
        const errors = {};
        const processedValues = {};

        this.props.inputs.forEach(
            ({
                id,
                required = true,
                validation,
                process = v => v,
                minLen = 0,
                maxLen = 20000
            }) => {
                const { value } = inputProps[id];
                errors[id] = validate(
                    {value, validation, required, minLen}
                );
                processedValues[id] = this.completeProcess(
                    value, process, maxLen
                );
            }
        );

        const errorFree = Object.values(errors).every(e => !e);

        if (errorFree) {
            this.props.onSubmit(processedValues);
        } else {
            const newState = {};

            Object.keys(inputProps).forEach(id => {
                const { value } = inputProps[id];
                const error = errors[id];
                newState[id] = { value, error };
            });

            this.setState(newState);
        }
    }

    renderInputs = () => {
        const { collect } = this.props;
        const collectors = { ...collect };
        const elements = [];


        this.props.inputs.forEach(({
            id,
            validation,
            required = true,
            submitOnEnter = true,
            label,
            initValue = "",
            className = "",
            placeholder = "",
            type = "text",
            tag = "input",
            options = [],
            process = v => v,
            minLen = 0,
            maxLen = 20000
        }) => {
            const value = this.state[id] ? this.state[id].value : initValue;
            const error = this.state[id] ? this.state[id].error : null;

            const onBlur = () => {
                const { followChange } = this.state[id];

                if (!followChange) {
                    this.setState({ [id]: {
                        ...this.state[id],
                        error: validate({value, validation, required, minLen}),
                        followChange: true
                    }});
                }
            };
            const onChange = newValue => {
                const { followChange } = this.state[id];
                const newState = {
                    ...this.state[id],
                    value: this.completeProcess(newValue, process, maxLen)
                };
                if (followChange) {
                    newState.error = validate(
                        {value: newValue, validation, required, minLen}
                    );
                }
                this.setState({ [id]: newState });
            };
            const onEnterKeyPress = ({ key }) => submitOnEnter && key === "Enter" && this.handleSubmit(); //eslint-disable-line

            const focus = () => {
                const { focusOn = {} } = this.props;
                if (id === focusOn.id && focusOn.event) {
                    return true;
                } else {
                    return false;
                }
            };
            const input = (
                <Input
                    className={className}
                    label={label}
                    placeholder={placeholder}
                    value={value}
                    onBlur={onBlur}
                    onChange={onChange}
                    type={type}
                    tag={tag}
                    options={options}
                    key={id}
                    id={id}
                    error={error}
                    onEnterKeyPress={onEnterKeyPress}
                    focusOn={focus()}
                />
            );
            if (Object.keys(collectors).length) {
                let found = false;
                Object.keys(collect).forEach(parentId => {
                    const children = collect[parentId];
                    children.forEach((childId, i) => {
                        if (childId === id) {
                            collectors[parentId][i] = input;
                            found = true;
                        }
                    });
                });
                if (!found) {
                    elements.push(input);
                }
            } else {
                elements.push(input);
            }
        });
        const collectorElements = Object.keys(collectors).map(parentId => (
            <div className={parentId} key={parentId}>
                {collectors[parentId]}
            </div>
        ));
        return [
            ...elements,
            ...collectorElements
        ];
    }
    render () {
        const {
            className,
            buttonText = "Submit",
            otherElements
        } = this.props;

        return (
            <div className={`form ${className}`}>
                {this.renderInputs()}
                {otherElements && otherElements.map((e, i) => e(i))}
                <button onClick={this.handleSubmit}>{buttonText}</button>
            </div>
        );
    }
}

export default Form;

输入是纯组件,但与问题无关。如您所见,我试图使组件尽可能地灵活,我只需要定义一些属性即可创建几乎每种类型的表单。但是这种灵活性要付出很高的代价,因为每次组件渲染时都需要处理属性。由于this.props.inputs不会100%更改,因此如果仅在安装组件时创建它就不会引起问题。将renderInputs移至componentDidMount,并将元素存储到this.state中,而不是将它们返回到render中是一个很好的解决方案吗?如果没有,那么对于这些​​问题最有效的解决方案是什么?

0 个答案:

没有答案