我希望我不是唯一一个处理大量数据以创建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中是一个很好的解决方案吗?如果没有,那么对于这些问题最有效的解决方案是什么?