Reputation: 739
I'm trying to create a form in my react app. I created an input.js component which is imported into my contact.js component and then mapped. I'm getting a "Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of input
" inside the console, but I don't understand why because every input component has a unique key already set. When I check them in chrome inspector's react tab, they all have a unique key set.
This is my contact.js component:
import React, { Component } from 'react';
import Input from './Input/input';
import Button from './Button/Button';
import Spinner from '../UI/Spinner/Spinner';
import {checkValidity} from '../../shared/utility';
import axios from '../../axios-contact';
class ContactForm extends Component {
state = {
contactForm: {
name: {
elementType: 'input',
elementConfig: {
inputprops: {
type: 'text',
id: 'name',
name: 'name',
required: 'required'
} ,
label: 'Name',
htmlFor: 'name',
invalid: 'Please enter your firstname',
value: '',
},
validation: {
required: true,
minLength: 2,
maxLength: 30
},
valid: false,
touched: false
},
company: {
elementType: 'input',
elementConfig: {
inputprops: {
type: 'text',
id: 'company',
name: 'company',
required: 'required'
},
label: 'Company',
htmlFor: 'company',
invalid: 'Please enter your company name',
value: '',
},
validation: {
required: true,
minLength: 2,
maxLength: 30
},
valid: false,
touched: false
},
location: {
elementType: 'input',
elementConfig: {
inputprops: {
type: 'text',
id: 'location',
name: 'location',
required: 'required'
},
label: 'Company location (city / country)',
htmlFor: 'location',
invalid: 'Please enter your location',
value: '',
},
validation: {
required: true,
minLength: 2,
maxLength: 30
},
valid: false,
touched: false
},
email: {
elementType: 'email',
elementConfig: {
inputprops: {
type: 'email',
id: 'email',
name: 'email',
required: 'required'
},
label: 'Email',
htmlFor: 'email',
invalid: 'Please enter a propper email address',
value: '',
},
validation: {
required: true,
isEmail: true,
minLength: 7,
maxLength: 40
},
valid: false,
touched: false
},
phone: {
elementType: 'input',
elementConfig: {
inputprops: {
type: 'tel',
id: 'phone',
name: 'phone',
required: false
},
label: 'Phone',
htmlFor: 'phone',
invalid: 'Please enter a propper phone number',
value: '',
},
validation: {
required: false,
minLength: 6,
maxLength: 30
},
valid: true,
touched: false
},
message: {
elementType: 'textarea',
elementConfig: {
inputprops: {
type: 'textarea',
id: 'message',
name: 'message',
required: 'required',
rows: 4
},
label: 'Message',
htmlFor: 'message',
invalid: 'Please enter a message',
value: '',
},
validation: {
required: true,
minLength: 2,
maxLength: 500
},
valid: false,
touched: false
},
compliance: {
elementType: 'checkbox',
containerClass: 'custom-control custom-checkbox',
inputClass: 'custom-control-input',
elementConfig: {
inputprops: {
type: 'checkbox',
id: 'gdpr',
name: 'gdpr',
required: 'required'
},
label: 'I consent to having this website store my submitted information so they can respond to my inquiry.',
htmlFor: 'gdpr',
invalid: 'Please give your consent before proceeding',
value: '',
},
validation: {
required: true,
isCheckbox: true,
isToggled: false
},
valid: false,
touched: false
}
},
formIsValid: false,
loading: false,
sent: false
}
contactHandler = ( event ) => {
event.preventDefault();
this.setState( { loading: true } );
const formData = {}
for (let formElementIdentifier in this.state.contactForm) {
formData[formElementIdentifier] = this.state.contactForm[formElementIdentifier].elementConfig.value;
}
axios.post('/contacts.json', formData)
.then(response => {
this.setState({ loading: false, sent: true });
console.log(formData);
})
.catch(error => {
this.setState({ loading: false, sent: true });
console.log(formData);
});
}
inputChangedHandler = (event, inputIdentifier) => {
const updatedContactForm = {
...this.state.contactForm
};
const updatedFormElement = {
...updatedContactForm[inputIdentifier]
};
updatedFormElement.elementConfig.value = event.target.value;
updatedFormElement.valid = checkValidity(updatedFormElement.elementConfig.value, updatedFormElement.validation);
updatedFormElement.touched = true;
updatedFormElement.validation.isToggled = !updatedFormElement.validation.isToggled;
updatedContactForm[inputIdentifier] = updatedFormElement;
let formIsValid = true;
for ( let inputIdentifier in updatedContactForm) {
formIsValid = updatedContactForm[inputIdentifier].valid && formIsValid;
}
this.setState({contactForm: updatedContactForm, formIsValid: formIsValid});
}
render () {
const formElementsArray = [];
for (let key in this.state.contactForm) {
formElementsArray.push({
id: key,
config: this.state.contactForm[key]
});
}
let form = (
<form onSubmit={this.contactHandler} name="contact">
{formElementsArray.map(formElement =>(
<Input
key={formElement.id}
elementType={formElement.config.elementType}
containerClass={formElement.config.containerClass}
inputClass={formElement.config.inputClass}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shoudValidate={formElement.config.validation}
touched={formElement.config.touched}
checked={formElement.config.validation.isToggled}
changed={(event) => this.inputChangedHandler(event, formElement.id)}
exited={(event) => this.inputChangedHandler(event, formElement.id)} />
))}
<Button disabled={!this.state.formIsValid} />
</form>
);
if (this.state.loading) {
form = <Spinner />
}
if (this.state.sent) {
form = <p id="contact-message" className="contact-message">Thank you for your message.<br /> We will respond as soon as possible.</p>
}
return (
<div className="contact">
<section id="contact-form" className="contact-form">
<h1>Contact</h1>
{form}
</section>
</div>
)
}
};
export default ContactForm;
and this is my input.js component:
import React from 'react';
import { NavLink } from 'react-router-dom';
const input = ( props ) => {
let label = <label htmlFor={props.elementConfig.htmlFor}>{props.elementConfig.label}</label>;
let inputElement = null;
let errorlabel = null;
let inputClass = ['input'];
const errorid = [props.elementConfig.id];
if(props.invalid && props.shoudValidate && props.touched) {
inputClass.push('error');
}
switch (props.elementType) {
case ('input'):
inputElement = <input
className ={inputClass.join(' ')}
{...props.elementConfig.inputprops}
value={props.elementConfig.value}
onChange={props.changed}
onBlur={props.exited} />;
break;
case ('email'):
inputElement = <input
className ={inputClass.join(' ')}
{...props.elementConfig.inputprops}
value={props.elementConfig.value}
onChange={props.changed}
onBlur={props.exited} />;
break;
case ( 'textarea' ):
inputElement = <textarea
className ={inputClass.join(' ')}
{...props.elementConfig.inputprops}
value={props.elementConfig.value}
onChange={props.changed}
onBlur={props.exited} />;
break;
case ( 'checkbox' ):
inputElement = <input
className ={[props.inputClass, inputClass.join(' ')].join(' ')}
{...props.elementConfig.inputprops}
value={!props.checked}
onChange={props.changed} />;
label = <label htmlFor={props.elementConfig.htmlFor} className="custom-control-label">This form collects your name, e-mail, phone number, company name, and location so that we may correspond with you. Read our <NavLink to="/privacy" exact>privacy policy</NavLink> for more information. By submitting the form, you consent to have StackApp collect the listed information.</label>;
break;
default:
inputElement = <input
className ={inputClass.join(' ')}
{...props.elementConfig.inputprops}
value={props.elementConfig.value}
onChange={props.changed}
onBlur={props.exited} />;
}
if(props.invalid && props.touched) {
errorlabel = <label id={errorid.join('-error')} className="error" htmlFor={props.elementConfig.htmlFor}>{props.elementConfig.invalid}</label>
};
let output = null;
if(props.elementType === 'checkbox') {
output = [inputElement, label, errorlabel];
} else {
output = [label, inputElement, errorlabel];
}
return (
<div role="group" className={props.containerClass}>
{output}
</div>
)
};
export default input;
What am I missing here?
Upvotes: 0
Views: 629
Reputation: 2390
Even though formElementsArray.map
seems like the most likely candidate, it is not the source of the warning in this case. Like you mentioned in the comments, each of your keys is unique by construction. The error comes from input.js where you assign output = [inputElement, label, errorlabel]
then render {output}
directly. React sees that this is an array, but doesn't know that it is of fixed size and expects each element in the array to have a unique key
prop. If you put a key
prop on inputElement
, label
, and errorLabel
the warning should go away.
Upvotes: 3