Mulitple dynamic inputs with state


(Saša Fišter) #1

I’m trying to solve this problem but I’m out of ideas. I have parent component which fetch categories from db (componentDidMount), and then pass it to child component. On child component I have multiple inputs that depends on how many categories are in parent comp. Basically everything works fine, but problem is how to update state on child component because I’m passing data from specific category. Here is the code:

Parent component

class Settings extends Component {
  constructor(props) {
  super(props)
  this.state = {
    categories: [],
    catCourses: [],
}
   this.handleChangeLimit = this.handleChangeLimit.bind(this)
   this.handleSubmit = this.handleSubmit.bind(this)
}

componentWillMount() {
  helpers.getCategories().then(response => {
     this.setState({categories: response.data})
   })
}

handleChangeLimit(e) {
  const value = e.target.value;
  const name = e.target.name;

this.setState({
    [name]: value
  })
}

handleSubmit(e) {
  e.preventDefault()
   helpers.updateCategoryShowLimit(this.state).then(response => {
      this.setState({categories: response.data.categories})
    }
})
}

render() {
  const cat = this.state.categories.map(category => {
    return(
      <CategoryItem category={category} key={category.id} handleChangeLimit={this.handleChangeLimit} />
     )
   })

return(
  <div className="content-inner">
        <form className="form-horizontal" onSubmit={this.handleSubmit}>
          {cat}
          <button className="btn btn-primary" type="submit">Save</button>
        </form>
    </div>
 )}
}

Child Component

const CategoryItem = (props) => {
  return(
   <div className="form-group row">
     <label className="col-sm-3 form-control-label">{props.category.name}</label>
     <div className="col-sm-9">
       <input type="text" className="form-control" 
         value={props.category.show_limit} 
         name={props.category.name} 
        onChange={e => props.handleChangeLimit(e)}/>
    </div>
  </div>
  )
}

(haxxxton) #2

it looks like you code should work other than how you’re calling this.setState. The way you update this will entirely depend upon the shape of your returned categories data. For example, if we assume the categories are an array of objects:

[
	{
		name: 'Category 1',
		show_limit: 4,
	},
	{
		name: 'Category 2',
		show_limit: 6,
	},
	...
]

Then you would need to find the index of the matching category, in order to update its show_limit. To do this you could leverage setState's ability to be passed a function rather than an object, something like:

this.setState((prevState) => {
	// treat state as if it was immutable
	return {
		// return the entire rest of prev state using the spread operator
		...prevState,
		// return a potentially modified version of the categories
		categories: prevState.categories.map(
			// for each category
			(category) => {
				// if the name matches (ideally an id) would be better
				if (category.name === name) {
					// update the show_limit with the event value
					return {
						...category,
						show_limit: value,
					}
				}
				// if we dont match on name, return the category
				return category;
			}
		),
	};
})

Here’s a nice little article about using a function to setState
Personally i find this a bit of a mess to have to do… so you could also have a look at some of the libraries available in redux’s immutable update patterns docs that make this kind of ‘deep’ updating easier.

An alternative would be to actually put ids on your categories, and pass that value through with the change handler, and potentially restructure your data to be an object of ids for faster searching/updating. For example:

{
	7: {
		id: 7,
		name: 'Category 1',
		show_limit: 4,
	},
	12: {
		id: 12,
		name: 'Category 2',
		show_limit: 6,
	},
	...
}

This would mean updating your render() iterator to use Object.values(this.state.categories).map() rather than how i assumed you had it at the moment (this.state.categories.map())

Then change your onChange prop

onChange={e => props.handleChangeLimit(props.id, e.target.value)}

And update your handleChangeLimit function

handleChangeLimit(id, value) {
	this.setState((prevState) => {
		// if the category with matching id doesnt exist yet just return prevState
		if (prevState.categories[id] === void 0) {
			return prevState;
		}
		// treat state as if it was immutable
		return {
			// return the entire rest of prev state using the spread operator
			...prevState,
			// return a modified version of the categories
			categories: {
				...prevState.categories,
				[id]: {
					...prevState.categories[id],
					show_limit: value,
				},
			},
		};
	})
}

(Saša Fišter) #3

Thnx haxxxton once again. I will check this, first I have another problem to solve as usual. I will come back to this and let you know, but I think that last example is ok. My return categories is this

the problem here is that i’m passing category and fetch value of it through props.category.show_limit, but I should use state because of updating values.


(haxxxton) #4

Based on your API response, you’re being returned an array, not an object, so the last example wont work ‘out-of-the-box’ you’d have to update your API response to return an object mapped to the id if you want the last example to work.

Your fetching function for your categories stores the result into your state object. so your Parent components render function should probably look something like this:

render() {
	const { categories } = this.state;
	return (
		<div>
			{
				categories.map(category => (
					<ChildComponent
						key={ category.id }
						handleChangeLimit={ this.handleChangeLimit }
						{ ...category }
					/>
				))
			}
		</div>
	);
}

This works because you’re passing into your Child component a function that modifies the parent’s state. You are also mapping over the parent components state object, so any changes to it will cause a re-render of the children.