Search code examples
reactjsformsreact-state-managementreact-state

In React, how do I name a field of my form that is part of an array?


I'm building a React 16.13.0 application. In my form, I want to submit data (an address) as part of an array, so I set up my state like so ...

  constructor(props) {
    super(props);

    this.state = {
      countries: [],
      provinces: [],
      errors: [],
      newCoop: {
        name: '',
        types: [],
        addresses: [{
          formatted: '',
          locality: {
            name: '',
            postal_code: '',
            state: ''
          },
          country: FormContainer.DEFAULT_COUNTRY,
        }],
        enabled: true,
        email: '',
        phone: '',
        web_site: '' 
      },

I then created these functions for managing changes to the input fields ...

  handleInput(e) {
    let self=this
    let value = e.target.value;
    let name = e.target.name;
    this.setValue(self.state.newCoop,name,value)
  }

  setValue = (obj,is, value) => {
       if (typeof is == 'string')
         return this.setValue(obj,is.split('.'), value);
       else if (is.length === 1 && value!==undefined) { 
         return this.setState({obj: obj[is[0]] = value});
       } else if (is.length === 0)
         return obj;
       else
         return this.setValue(obj[is[0]],is.slice(1), value);
  }

...
                <Input inputType={'text'}
                   title= {'Street'} 
                   name= {'addresses[0].formatted'}
                   value={this.state.newCoop.addresses[0].formatted} 
                   placeholder = {'Enter address street'}
                   handleChange = {this.handleInput}
                   errors = {this.state.errors} 
                  /> {/* Address street of the cooperative */}

The Input.jsx file looks like the below ...

const Input = (props) => {
    return (  
  <div className="form-group">
      <FormLabel>{props.title}</FormLabel>
      <FormControl
            isInvalid={props.errors && Boolean(props.errors[props.name])}
            type={props.type}
            id={props.name}
            name={props.name}
            value={props.value}
            placeholder={props.placeholder}
            onChange={props.handleChange}
          />

      {props.errors && props.errors[props.name] && (
          <FormControl.Feedback type="invalid">
                 {props.errors[props.name].map((error, index) => (
                     <div key={`field-error-${props.name}-${index}`} className="fieldError">{error}</div>
                 ))} 
          </FormControl.Feedback>
      )}
  </div>
    )
}

export default Input;

However, when I attempt to change the value, I get the below error. I'm not sure what else I need to be doing to name my component such that I can successfully change it's value. I would prefer not to change the data structure in my constructor, but I'm willing to if that's what it takes.

TypeError: Cannot set property 'formatted' of undefined
FormContainer.setValue
src/containers/FormContainer.jsx:127
  124 | if (typeof is == 'string')
  125 |   return this.setValue(obj,is.split('.'), value);
  126 | else if (is.length === 1 && value!==undefined) { 
> 127 |   return this.setState({obj: obj[is[0]] = value});
      | ^
  128 | } else if (is.length === 0)
  129 |   return obj;
  130 | else

Solution

  • ISSUE:

    Cannot set property 'formatted' of undefined
    
    // Reason : because you can't access obj["addresses[0]"]["formatted"]
    
    // Solution : it should look something like obj["addresses"][0]["formatted"] 
    

    Because you are splitting up string by ., so a result you are getting

    [
      "addresses[0]",
      "formatted"
    ]
    

    Now that you have successfully splitted up the string ,

    You are trying to get object by name, specifically obj["addresses[0]"], But you can't access the object index like this,

    It will give you undefined, so as a result, you are getting the above error. you can check that exact error by running below code snippet,

    const obj = {
        name: '',
        types: [],
        addresses: [{
            formatted: '',
            locality: {
                name: '',
                postal_code: '',
                state: ''
          },
        }],
    };
    
    const names = "addresses[0].formatted".split(".")
    
    console.log("obj['addresses[0]'] ===>" , obj[names[0]])
    
    console.log("obj['addresses[0]']['formatted'] ===>" , obj[names[0]][names[1]])


    SOLUTION :

    So now question is if not obj["addresses[0]"] this then what, the solution is obj["addresses"]["0"],

    So you have 2 options :

    First : change this addresses[0].formatted to addresses.0.formatted

    Second : you need to split the sting with .split(/[\[\].]+/)

    I would prefer second option as this addresses[0].formatted looks real form name, and this is how it should look like, you can check that in below code snippet also.

    const obj = {
        name: '',
        types: [],
        addresses: [{
            formatted: '',
            locality: {
                name: '',
                postal_code: '',
                state: ''
          },
        }],
    };
    
    const names = "addresses[0].formatted".split(/[\[\].]+/)
    
    console.log("obj['addresses'] ==>" , obj[names[0]])
    console.log("obj['addresses']['0'] ==>" , obj[names[0]][names[1]])
    console.log("obj['addresses']['0']['formatted'] ==>" , obj[names[0]][names[1]][names[2]])


    NOTE :

    Now, once you solved the issue, real issue come up in the picture, obj: obj[is[0]] = value, here obj is object so this will throw error , and also your setValue function is limited to that functionality only, it should be generic

    handleInput = e => {
        let name = e.target.name;
        let value = e.target.value;
        const keys = name.split(/[\[\].]+/);
        this.setState(this.updateValue(this.state, keys, value));
    };
    
    // I've created a recursive function such that it will create a 
    // copy of nested object so that it won't mutate state directly
    
    // obj : your state
    // name : input name
    // value : value that you want to update
    updateValue = (obj, name, value, index = 0) => {
        if (name.length - 1 > index) {
            const isArray = Array.isArray(obj[name[index]]);
            obj[name[index]] = this.updateValue(
                isArray ? [...obj[name[index]]] : { ...obj[name[index]] },
                name,
                value,
                index + 1
            );
        } else {
            obj = { ...obj, [name[index]]: value };
        }
        return obj;
    };
    

    WORKING DEMO :

    Edit #SO-recursive-form-setval