Generate options by mapping over array of objects with react-select - javascript

I'm using react-select to create a Select option in my create-react-app and am trying to map over an array of objects to generate the options. My app loads fine but when I click on the Select I get this error: Uncaught Invariant Violation: Objects are not valid as a React child (found: object with keys {name}). If you meant to render a collection of children, use an array instead.
I'm passing the data to the component via props which is working fine, and the data is structured like this:
const guests = [
{
name: 'Kait',
plus: true,
plusName: 'Kitty'
},
{
name: 'Séanin',
plus: true,
plusName: 'Guest'
}
]
And here's the Select component:
<Select
value={selectedOption}
onChange={this.handleChange}
options={
this.props.guests.map((guest, index) => {
return {
label: guest,
value: guest,
key: index
}
})
}
/>
Any ideas on how I can fix this?

Sung M. Kim‘s answer is correct, but there is an easier way to use your attributes as label and value without remapping your options array.
Using the props getOptionLabel and getOptionValue you can keep your object mappings. Both accept a function that gets a single option as an argument and returns the value or label from the appropriate object property as string.
<Select
options={this.props.guests}
getOptionLabel={(option) => option.name}
{ /* Couldn't find a value in your structure, so I used name again */ }
getOptionValue=((option) => option.name}
{ ... }
/>
See documentation for more.

You probably will have to generate the array before rendering the component
const options = this.props.guests.map((guest, index) => {
return {
label: guest.name,
value: guest,
key: index
}
})
<Select
value={selectedOption}
onChange={this.handleChange}
options={options}
/>
Edit:
is because you are passing an object in the label field. You should pass a String instead

The error occurs because the label is set as guest (an object) not as guest.name (a string).
Making following change will work.
<Select
value={selectedOption}
onChange={this.handleChange}
options={
this.props.guests.map((guest, index) => {
return {
- label: guest,
+ label: guest.name
value: guest,
key: index
}
})
}
/>
You can try it out in the sandbox link below.

Related

How to render multiple dynamic options into select box in React from Axios

I have a perplexing question. I am receiving a payload from an axios call that I need to populate in a select (dropdown) box in React--except the payload I receive is an array of arrays. There is a large array of (2) Two element arrays received, the two elements being a shorthand ID name, and the fullname. I need to concatenate these two values together into one option. For instance the structure of the data received:
{
"groups": [
[
"short_name_id",
"longer_name"
]
]
The data in the select box should be a concatenation of the two elements like: "short_name_id - longer_name" I don't think I have to manually concatenate both the two values together and then put in state, but to show each of them together in each select dropdown choice.
In the call I have an axios call that returns the data correctly:
componentDidMount() {
axios.get('https://someurl.com/api/groups')
.then(response => {
console.log(response);
this.setState({ data: response.data });
})
.catch(error => console.log(error.response));
}
In the render() I set the props to the data object returned but the select box doesn't populate?
Not sure how to get the two array elements to show up as one entry and populate the state into the select box? I am not accessing/referencing the data that is in state properly considering the array structure I don't think?
render() {
const { groups } = this.props;
return (
<label>
Group:
<select onChange={this.handleInputChange}>
{groups &&
groups.length > 0 &&
groups.map(group => {
return <option key={group} value={group}>{group}</option>;
})}
</select>
</label>
);
}
render() {
const { groups } = this.props;
return (
<label>
Group:
<select onChange={this.handleInputChange}>
{groups &&
groups.length > 0 &&
groups.map(group => {
return <option key={group[0]} value={group[0]}>{group[0]} - {group[1]}</option>;
})}
</select>
</label>
);
}
This should work, but personally I wouldn't store the data as array of arrays. I would rather store them as array of objects, so instead of looking like this:
[
"short_name_id",
"longer_name"
]
It would look like this:
{
"short_name_id": "value here",
"longer_name": "value here"
}
This way your return statement instead of looking like this:
return <option key={group[0]} value={group[0]}>{group[0]} - {group[1]}</option>;
could look like this:
return <option key={group.short_name_id} value={group.short_name_id}>{group.short_name_id} - {group.longer_name}</option>;

react dynamic form values with antd

Dynamic forms with react and antd are eluding me. I have scoured the web looking for answers to no avail. Here is a codepen with a recreation of the issue I am having: https://codepen.io/sethen/pen/RwrrmVw
Essentially, the issue boils down to when you want loop through a bunch of values that are stored in state, like so:
class MyClass extends React.Component<{}, {}> {
constructor(props) {
super(props);
this.state = {
data: [
{ name: 'foo' },
{ name: 'bar' },
{ name: 'baz' }
]
};
}
You can think of these values as being fetched from some remote API.
As you can see, I have an array of objects with the key of name in the state. Further on down in the render cycle is the following:
return data.map((value, index) => {
const { name } = value;
return (
<Form key={ index } initialValues={ { name } }>
<Form.Item name='name'>
<Input type='text' />
</Form.Item>
<Button onClick={ this.handleOnDeleteClick.bind(this, index) }>Delete</Button>
</Form>
);
This attempts to loop through the values stored in the state and put the values into an input. It also adds a little delete button to get rid of that item. The first time it renders, it does as you expect it to loading the value into the input value.
The issue is when you try to delete one of the items, like the middle one, it will delete the next item. The core of the issue is that the render is acting different than I expect it to when deleting an item. I am expecting that when I delete an item, it will take it out of state and load the ones that are left. This is not happening.
My question is, how am I able to load dynamic data in this way with antd whilst being able to delete each item?
The main mistake in this form that you assign the key property as the array index, and on deleting the middle item, the last component will get a new key.
In React, changing the key will unmount the component and lose its state.
Don’t pass something like Math.random() to keys. It is important that keys have a “stable identity” across re-renders so that React can determine when items are added, removed, or re-ordered. Ideally, keys should correspond to unique and stable identifiers coming from your data, such as post.id.
Also, in your example, you actually render three forms instead of a single form and three fields.
Every <form/> has in its inner state all states of its form fields, so you will have a single object with all input values in it.
Antd.Form just a wrapper for such form, you can get Form.Item values in onFinish callback for example.
class MyClass extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [{ name: "foo" }, { name: "bar" }, { name: "baz" }]
};
}
handleOnDeleteClick = index => {
this.setState({
data: [
...this.state.data.slice(0, index),
...this.state.data.slice(index + 1)
]
});
};
render() {
const { data } = this.state;
return (
<Form>
{data.map(({ name }, index) => {
return (
<Form.Item key={name}>
<Input type="text" />
<Button onClick={() => this.handleOnDeleteClick(index)}>
Delete
</Button>
</Form.Item>
);
})}
</Form>
);
}
}

React rerender component using hooks when property in an array of object changes

So I am in a situation where I have to change a particular property from an array of objects. When the property changes I want to rerender the component. Now, this works fine without any issues when use the setPropertyName of useState. But now I am just changing one property of the object instead of the entire object.
Here is the code that Im working on:
const [movieList, setMovieList] = useState([]);
Calling the setMovieList and passing an array will obviously cause a rerender.
Consider the following contents of movieList:
movieList = [
{
'name': 'Mulholland Dr.'
'year':2001,
'watched' : true,
'rating':0
},
{
'name': 'Zodiac'
'year':2007,
'watched' : false,
'rating':0
},
{
'name': 'Twin Peaks'
'year':2017,
'watched' : true,
'rating': 0
}]
Then I have a function which renders the list:
function showMovieList () {
return movieList.map((movie) => {
return (
<List.Item key={movie.imdbID}>
<div className="watchedCheckBoxContainer">
<input type="checkbox" onChange={(event) => movie.watched = event.target.checked} id={`cb1${movie.imdbID}`}/>
<label htmlFor={`cb1${movie.imdbID}`}><Image size='tiny' src={movie.Poster} /></label>
</div>
{/* <Image size='tiny' src={movie.Poster} /> */}
<List.Content>{movie.Title}</List.Content>
{movie.watched ? <Rating maxRating={5} onRate={(event, {rating}) => movie.userRating=rating}/> : null}
</List.Item>
)
});
}
As you can see , when the checkbox is clicked it changes the value of the watched property. A few lines later I'm checking if movie.watched == true then show the <Rating> component. But in this case, I'm not using setMoviesList to update the moviesList and hence the <Rating> component is not visible.
How can I use setMoviesList to update watched property of the particular movie whose checkbox I click on?
Okay.. I solved it by the following way:
function onMovieWatched (watched, index) {
const tempMoviesList = [...movieList];
tempMoviesList[index].watched = watched;
setMovieList(tempMoviesList);
}
<input type="checkbox" onChange={(event) => onMovieWatched(event.target.checked, idx)} id={`cb1${movie.imdbID}`}/>
The idx is the index that I am using from the map method.
Initially I was afraid that I might have to loop over the entire array and get the object that matches the imdbID and then update its property.
Luckily I have the index while mapping over it, so I just used that to directly retrieve the object.
Dont know why I didnt think of this solution before posting.

Immutable .toArray() wont render data - ReactJS

I am mapping over some data and I am trying to use some of the data as the options for the semantic-ui-react Dropdown component, but the data wont load in even if I add .toArray() to the end of it, however if I change the data structure to not use Immutable, it works fine. I mainly wanted to understand why so I could get a better understanding of things.
Here is the mapping of the data with immutable:
{
labelingData.map((item, i) => {
return (
<span key={i}>
{
item.get('categories').map((category, c) => {
return (
<span key={c}>
{
category.get('subcategories').map((subcategory, sc) => {
return (
<span key={sc}>
{
subcategory.get('name') === 'Labels'
? <Dropdown
defaultValue={'Bananas'}
options={subcategory.get('childItems').toArray()}
/>
: null
}
</span>
);
})
}
</span>
);
})
}
</span>
);
})
}
I do get an error in the console about unknown props but I am not adding these props in my code:
Unknown props `_root`, `__ownerID`, `__hash`, `__altered` on <div> tag. Remove these props from the element
The api of semantic-ui-react Dropdown options is to pass in an array of objects that look like:
{ text: '', value: '' }
However, when you call .toArray() on a Immutable.List containing some Immutable.Map it will not .toJS() the Maps.
eg.
var list = Immutable.fromJS({ value: 1 }, 2]);
console.log(list.toArray()); // prints [Immutable.Map, 2]
So you are actually giving Dropdown an array of Maps and the Dropdown doesn't use .get to get the values from the object.
Another thing is that React prefers to receive arrays rather than immutable.Lists.
So you could use a reduce function rather than a map function to create a normal array of react components:
item.get('categories').reduce((result, category, c) => {
result.push(<Category key={c} category={category} />);
return result;
}, []);

Populate 2nd dropdown based on selection from 1st dropdown in React.Js

I am learning react and struggling to get a second dropdown to populate based on which option is clicked from first dropdown.
I have included my code below.
I think the issue is where I try to set this.state.selected = param.tableName. I don't think that will work, but I'm not sure what to use instead.
I need an onClick event to change the this.state.selected variable to the option that is selected I think, and then check when tableName === selected. I will also include my JSON below so the context is more clear.
import React from 'react';
import axios from 'axios';
class Search extends React.Component {
constructor(props) {
super(props)
this.state = {
params: [],
columnParams: [],
selected: ''
}
}
componentWillMount() {
axios.get('http://localhost:3010/api/schema')
.then( response => {
this.setState({params: response.data})
})
}
render() {
return (
<div className="Search">
<select>{this.state.params.map((param, i) =>
<option key={i} onClick={this.setState(this.state.selected ={param.tableName})}>
{param.tableName}
</option>)}
</select>
<select>
{
this.state.params
.filter(({tableName}) => tableName === selected)
.map(({columns}) => columns.map((col) =><option>{col}</option>))}
[
{
"tableName": "customer",
"columns": [
"customerid",
"title",
"prefix",
"firstname",
"lastname",
"suffix",
"phone",
"email"
]
},
{
"tableName": "product",
"columns": [
"productid",
"name",
"color",
"price",
"productadjective",
"productmaterial"
]
},
You're doing it wrong way. You cannot call onClick method on <option>. Instead you should use onChange method on <select> (see react docs) like this:
<select onChange={this.handleChange} >
<option>1</option>
<option>2</option>
<option>3</option>
</select>
Then on 'onChange' event you can set your state to the selected option. Let's understand this scenario with an example.
Suppose you have two dropdowns. One for showing companies and one for jobs corresponding to each company. Each company has its own set of jobs like this:
companies:[
{ name: 'company1', jobs: ['job1-1', 'job1-2', 'job1-3']},
{ name: 'company2', jobs: ['job2-1', 'job2-2', 'job2-3']},
{ name: 'company3', jobs: ['job3-1', 'job3-2', 'job3-3']}
]
Now you have to just set a state, lets say 'selectedCompany' and then filter the companies array by 'selectedCompany'. Then you can just get that particular company object and map your 'jobs' dropdown by iterating over jobs array inside the company object.
Here is the code snippet:
https://codepen.io/madhurgarg71/pen/pRpoMw
event handlers for e.g. onClick must be a function
this.setState expects an object, which will be merged with the state
so the part where you set selected in your state must be
<option key={i} onClick={() => {
this.setState({selected: param.tableName})
}>{param.tableName}}}</option>
you use an undefined variable in your filter (selected),
you have to use .filter(({tableName}) => tableName === this.state.selected) instead
Some things to look at:
The syntax in your call to setState in your <option> onClick event handler looks incorrect.
You also make try to reference this.state.selected in your .filter condition without the path to the variable (this.state)
Suggested solution(s):
// The .setState method expects an object to be passed to it rather
// than an expression that mutates the state directly
// Pass it an object containing the properties you want to change,
// ensure your use colon (':') rather than equals sign ('=') to set property:
<option key={i} onClick={
this.setState({
selected: param.tableName
})
</option>
// In the <select> tag,
// you need to refer to the selected property in it as this.state.selected:
// Don't forget the closing select tag, it's missing from your snippet
<select>
{
this.state.params
.filter(tableName => (tableName === this.state.selected))
.map(columns => columns.map(col => <option>{col}</option>))
}
</select>
This issue can easily be dealt with by chaining a filter and map together on the second input, which filters it's options depending on the input of the first drop down.
<select value={country} onChange={e => setCountry(e.target.value)}>
{countries.map(country => (
<option value={country}>{country}</option>
))}
</select>
<select value={school} onChange={e => setSchool(e.target.value)}>
{schools.filter(school => school.Country === country).map(school => (
<option value={school.Name}>{school.Name}</option>
))}
</select>
Above we have two selects. The first selects a value, and then the options for the second will render based on this.

Categories