2

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
Dave
  • 15,639
  • 133
  • 442
  • 830

2 Answers2

3

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

Vivek Doshi
  • 56,649
  • 12
  • 110
  • 122
0

Your code is quite confusing, that's part of your problem to begin with, the other problem with your code is that it is not good practice to have nested objects in react's state. You can learn more by reading this answer in this other question.

Here is an example of what you could do with your code to set the state, however, notice that this is a bad way of solving the issue:

handleInput(e) {
  let value = e.target.value;
  this.setState(prevState =>{
  ...prevState,
  newCoop: {
    ...prevState.newCoop
    addresses: [
        {
        ...prevState.newCoop[0].addresses
        formatted: value
        }
      ]
  }
  })
}
Jose V
  • 1,655
  • 1
  • 17
  • 31