Goran Tesic
Goran Tesic

Reputation: 739

Why am I getting a "unique key prop" warning when my mapped input components already have a unique key?

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

Answers (1)

Tyler
Tyler

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

Related Questions