I have a list of inputs with an empty input at the end.
If a user types in this empty input, the state is changed to reflect this, and a new blank input is added to the end.
If the user deletes the value in the input at the end (not the blank one), this input is removed and we still have a blank input at the end.
The issue: There are at least 4 inputs (first 3 with values, last blank as expected). The user makes input 2 blank.
Expected result: Inputs are re-rendered in the new order (Input1, Input3, BlankInput)
Actual result: Inputs rendered in this order (Input 1, BlankInput, Input3)
Code:
type Props = {}
const InitialState = {
data: [],
}
type State = typeof InitialState
export default class List extends React.Component<Props, State> {
public state = InitialState
componentDidMount() {
var inputs = [];
//Call API to get data
getList().then((dataArray) => {
inputs = JSON.parse(dataArray)
this.setState({ data: inputs })
});
}
generateInput(input: any) {
var listArray = input.config.order.split(',');
listArray.push('');
return listArray;
}
delaySaveOfList(e: React.ChangeEvent<any>, input: any, name: string) {
var listArray = input.config.order.split(',')
var arrTarget = Number(e.target.name);
var inputLength = e.target.parentElement.querySelectorAll("#ListInput").length - 1
if (e.target.value == "" && inputLength > 1) {
var newInputArray = [] as any;
this.state.data.forEach((inputState: any) => {
if (inputState === input) {
listArray.splice(arrTarget, 1);
input.config.order = listArray.toString();
newInputArray.push(input)
}
else {
newInputArray.push(inputState)
}
})
this.setState({ data: newInputArray })
//Save this new list to API
updateList(input);
}
}
onChange(e: React.ChangeEvent<any>, input: any, name: string) {
e.persist();
if (e.target.refreshTimer) {
clearTimeout(e.target.refreshTimer);
}
e.target.refreshTimer = setTimeout(() => {
this.delaySaveOfList(e, input, name)
}, 1000);
};
render() {
return (
<div>
<h1>Inputs</h1>
{this.state.data.map((input: any) => (
<div className="card">
<div className="card-body">
<p className="card-text">Inputs:</p>
{this.generateInput(input).map((name: any, index: number) => (
<>
<input id="ListInput" type="text" defaultValue={name} name={index.toString()} key={index} onChange={(e) => { this.onChange(e, input, name) }} />
<br />
</>
))}
</div>
</div>
))}
</div>
)
}
}
I will say, once I refresh the page, the order of the inputs is correct, as it pulls data from the API again, so I assume I'm doing something wrong with the state data.
im prety new to React and im trying to use an autocomplete input. Im having problems getting the value from it and clearing the input values after submitting. Any help would be greatly appretiated.
import React, { Component, Fragment } from "react";
import PropTypes from "prop-types";
import "../AutoComplete/styles.css"
class Autocomplete extends Component {
static propTypes = {
suggestions: PropTypes.instanceOf(Array)
};
static defaultProps = {
suggestions: [],
};
constructor(props) {
super(props);
this.state = {
// The active selection's index
activeSuggestion: 0,
// The suggestions that match the user's input
filteredSuggestions: [],
// Whether or not the suggestion list is shown
showSuggestions: false,
// What the user has entered
userInput: this.props.value ? this.props.value : "",
};
}
//Order by 'code'
generateSortFn(prop, reverse) {
return function (a, b) {
if (a[prop] < b[prop]) return reverse ? -1 : 1;
if (a[prop] > b[prop]) return reverse ? 1 : -1;
return 0;
};
}
onChange = e => {
const { suggestions } = this.props;
const userInput = e.currentTarget.value;
// Filter our suggestions that don't contain the user's input
const filteredSuggestions = suggestions.sort(this.generateSortFn('code', true)).filter(
(suggestion, i) => {
let aux = suggestion.descrp+"- "+suggestion.code
return aux.toLowerCase().indexOf(userInput.toLowerCase()) > -1
}
);
this.setState({
activeSuggestion: 0,
filteredSuggestions,
showSuggestions: true,
userInput: e.currentTarget.value
});
};
onClick = e => {
this.setState({
activeSuggestion: 0,
filteredSuggestions: [],
showSuggestions: false,
userInput: e.currentTarget.innerText
});
};
onKeyDown = e => {
const { activeSuggestion, filteredSuggestions } = this.state;
// User pressed the enter key
if (e.keyCode === 13) {
this.setState({
activeSuggestion: 0,
showSuggestions: false,
userInput: filteredSuggestions[activeSuggestion].code+" - "+filteredSuggestions[activeSuggestion].descrp
});
}
// User pressed the up arrow
else if (e.keyCode === 38) {
if (activeSuggestion === 0) {
return;
}
this.setState({ activeSuggestion: activeSuggestion - 1 });
}
// User pressed the down arrow
else if (e.keyCode === 40) {
if (activeSuggestion - 1 === filteredSuggestions.length) {
return;
}
this.setState({ activeSuggestion: activeSuggestion + 1 });
}
};
render() {
const {
onChange,
onClick,
onKeyDown,
state: {
activeSuggestion,
filteredSuggestions,
showSuggestions,
userInput
}
} = this;
let suggestionsListComponent;
if (showSuggestions && userInput) {
if (filteredSuggestions.length) {
suggestionsListComponent = (
<ul className="suggestions">
{filteredSuggestions.map((suggestion, index) => {
let className="";
// Flag the active suggestion with a class
if (index === activeSuggestion) {
className = "suggestion-active";
}
return (
<li className={className} key={suggestion.code} onClick={onClick}>
{suggestion.code+" - "+suggestion.descrp}
</li>
);
})}
</ul>
);
} else {
suggestionsListComponent = (
<div className="no-suggestions">
<p>Sin sugerencias</p>
</div>
);
}
}
and the return (this is where i think im wrong)
return (
<Fragment>
<label htmlFor="autocomplete-input" className="autocompleteLabel">{this.props.label}</label>
<div className="centerInput">
<input
className="autocomplete-input"
type="text"
onChange={onChange}
onKeyDown={onKeyDown}
defaultValue={this.props.initState}
value= {/* this.props.value ? this.props.value : */ userInput}
placeholder={this.props.placeholder}
selection={this.setState(this.props.selection)}
/>
{suggestionsListComponent}
</div>
</Fragment>
);
}
}
export default Autocomplete;
What I want is to use this component in different pages, so im passing the "selection" prop and setting the state there.
The input is working correctly (searches, gets the value and shows/hide the helper perfectly). The problem is i cant reset this inputs clearing them, and i suspect the error is in here.
I get the following warning (even with it somewhat functioning)
Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
This is the Component usage with useState:
<Autocomplete label='Out cost Center:' placeholder='Set the out cost center' suggestions={dataCostCenterHelper} selection={(text) => setOutCostCenter(text.userInput)} value={outCostCenter} />
and last this is how im tryin to clear the state that is set in "selection":
const clearData = async () => {
setOutCostCenter('-');
// other inputs with the same component
setOutVendor('-');
setOutRefNumber('-');
}
This gets called inside the function that handles the button submitting the form.
Thanks in advance!
Looking at the code you posted this line might be the problem:
selection={this.setState(this.props.selection)}
You are updating state directly inside the render method, this is not recommended.
Try using a selection prop or state field and update the prop inside a componenteDidMount life cycle
selection={this.state.selection}
I have a react-table where I'm generating about 100 checkboxes dynamically. I have tried everything I can think of, but checking/unchecking the checkboxes takes a very long time before I can see any changes. It seems to be re-rendering every single checkbox before I can see a tick on the checkbox I just clicked on (the page freezes for about 5-10 secs). I'm using a change handler defined in a wrapper component passing as a prop.
// Generate columns for table
// this is added to the column props of React-Table
function setActionsColumns() {
return rolesActionsList.map((action, index) => {
const actionName = get(action, 'name');
return {
Header: actionName,
accessor: actionName,
Cell: row => {
const objectName = get(row, 'original.name');
let data = formData.permissions.filter(
perm => row.tdProps.rest.action === perm.action
);
let roleData = data[0];
let checked;
if (get(roleData, 'object') === row.tdProps.rest.object) {
checked = get(roleData, 'checked');
}
return (
<Checkbox
key={`${actionName}_${index}`}
name={actionName}
onChange={onCheckboxChange}
data-object={objectName}
checked={checked}
/>
);
},
};
});
}
// Checkbox handler defined in a class component
handleCheckboxChange = (event: SyntheticEvent<HTMLInputElement>) => {
const { roleData } = this.state;
const target = event.currentTarget;
const actionName = target.name;
const checked = target.checked;
const objectName = target.dataset.object.toUpperCase();
const checkedItem = {
action: actionName,
object: objectName,
checked,
};
let checkedItemsList = [];
let isInArray = roleData.permissions.some(perm => {
return perm.action && perm.object === checkedItem.object;
});
if (!isInArray) {
checkedItemsList = [...roleData.permissions, checkedItem];
} else {
checkedItemsList = roleData.permissions.filter(
perm => perm.action !== checkedItem.action
);
}
this.setState({
roleData: {
...this.state.roleData,
permissions: checkedItemsList,
},
});
};
I am using FormSpy in the child file and have access to this.props.form in the current file.
The form values that I want to have are currently in the local state in the child file and I have been trying to update form values using form.change() but they are not updated.
Shortened code to highlight the issue of trying to update form values based on state.
class ABC extends Component {
this.state = {
segmentMappings: {
id: '',
name: '',
statusId: ''
}
};
handleStatusChange = (e) => {
var segmentMappings = [...this.state.segmentMappings];
var newStatusId = 8;
// Logic to update this.state.segmentMappings
var name = `segments${e}.statusId`;
console.log('Field name', name);
//Form values are not updating form value as I was expecting
this.props.form.change(name, newStatusId);
this.setState({ segmentMappings }
}
}
const SegmentTile = {
return null;
}
//I want to render the names prop and save id & statusId values to the form.
segmentsData(segmentsByApp) {
let result = [];
let i = 0;
segmentsByApp.map((seg) => {
result.push(
<div
key={seg.id} onClick={() => this.handleStatusChange(seg.id)}>
<Field
name={`segments[${seg.id}].id`} value={seg.id}
component={SegmentTile}></Field>
<Field
name={`segments${seg.id}.statusId`} value={seg.statusId}
component={SegmentTile}></Field>
<Segment>
<span>{seg.name}</span>
</Segment>
</div >
);
result.push(<SegmentDivider key={seg.id + 'divider'} />);
i++;
}
}
);
return result;
};
render() {
{this.segmentsData(this.state.segmentMappings)}
}
}
I hope I'm not the only one who's facing with the problem of processing large amount of data to create JSX elements. The problem with that in my solution, that every time a state or prop changes inside a component, it does the whole processing to create it's children, unnecessarily.
Here's my component:
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;
Where Input is a Pure Component, but it's not relevant to the question. So as you can see, I tried to make the component as flexible as I can, where I need to define just a few attributes to create almost every type of form. But this flexibility costs much, as it needs to process the properties every time the component renders. As this.props.inputs will not change 100%, it wouldn't cause issues if it would be created just when the component mounts. Is it a good solution to move renderInputs to componentDidMount, and store the elements into this.state instead of returning them inside render? If not, what would be the most efficient solution for those problems?