Imagine a simple React component with <select> element that allows to choose a city based on country. For example
<MyCitySelectComponent
country={ 'France' }
city={ 'Paris' }
onChange={ someFunction }
/>
When mounted, it should load list of available Cities (based on Country) and render <select>.
When city property is changed - it should modify <select> input value and trigger onChange event.
When country property is changed (from parent component) - it should reload list of available cities from a remote server and trigger the same onChange event.
I managed to implement first two, here is simplified code:
class MyCitySelectComponent extends Component {
constructor(props) {
super(...props);
this.state = {
cities: null,
city: props.city,
country: props.country
};
}
onCityChange( e ) {
this.setState({
city: e.target.value
});
this.props.onChange( e.target.value );
}
loadCities() {
fetch({
path: '/get/cities?country=' + this.state.country,
}).then( cities => {
this.setState({
cities: cities
});
});
}
componentDidMount() {
this.loadCities();
}
render() {
if ( !this.state.cities ) {
// not loaded yet
return null;
}
return (
<select>
{ this.state.cities.map( ( name, index ) =>
<option
value={ name }
onChange={ this.onCityChange }
selected={ name === this.state.city }
/>
) }
</select>
)
}
}
But I'm having trouble reloading cities when country is changed dynamically from parent component. I tried using shouldComponentUpdate, but all I get is infinite loops.
Is there any pattern for such type of component?
Thank you.
Fetching new data based on prop changes should be handled in componentDidUpdate or getDerivedStateFromProps. Have a look at the docs for an example: https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data-when-props-change
Note that componentWillReceiveProps is deprecated!
Related
I have the following code where I am trying to update the value for the select tag.
constructor(props){
super(props)
this.state={value: 'Male'}
}
handleChange = (event) => {
this.setState({value: event.target.value})
this.props.selectCB(this.state.value)
console.log(this.state.value)
}
render(){
return (
<label>GENDER: <br/>
<select value={this.state.value} onChange={this.handleChange}>
<option value='Male'>Male</option>
<option value='Female'>Female</option>
<option value='Not Specified'>Not-Specified</option>
<option value='Non Binary'>Non-Binary</option>
</select>
<br/>
</label>
)
}
}
class NameForm extends React.Component{
constructor(props){
super(props)
this.state = {selectValue: ''}
}
handleSelectCallback = (selectData) => {
this.setState({selectValue: selectData})
}
handleSubmit = (event) => {
console.log('Logged select: ' + this.state.selectValue)
alert(`Submitted : ${this.state.selectValue}`)
event.preventDefault()
}
render(){
return <form onSubmit={this.handleSubmit}>
<SelectTag selectCB={this.handleSelectCallback}/>
<input type='submit' value='Submit'></input>
</form>
}
}
function App(){
return <NameForm/>
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(App());
The SelectTag is a child component to NameForm which in turn is rendered by the function App(). The change handler resides in SelectTag while the submit handler is in NameForm. I am trying to get the selected value from SelectTag to the parent NameForm by using a callback handleSelectCallback(). When the data in SelectTag is changed it is not being updated in NameForm.
If I start with the value Male and change it to Female, the value of selectValue in NameTag is still Male. If I change the value again (say to Not Specified), the value of selectValue changes to Female.
(Note: I noticed that this is working properly for other React components. I tested with components that render text boxes and text areas.)
You are sending the old state value through the callback. setState is async.
handleChange = (event) => {
// Schedule an update to the component with a new state value
this.setState({value: event.target.value})
// Callback with the outdated state value
this.props.selectCB(this.state.value)
}
You could just change it to the right value
handleChange = (event) => {
this.setState({value: event.target.value})
this.props.selectCB(event.target.value)
}
Or better yet, remove the state.value because it's not need. Instead keep the state in one place (the parent), and send the value and the callback down.
handleChange = (event) => {
this.props.selectCB(event.target.value)
}
// and
<select value={this.props.value} />
// and
<SelectTag selectCB={this.handleSelectCallback} value={this.state.selectValue} />
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>
);
}
}
I recently got started with React and want to build a little application to fetch weather data. My API has a function to return autocomplete suggestions. So when my autosuggestion array is not empty I render a list and upon clicking one of the <li>'s I want the value inside of the input box. I manage to set the state of my SearchBar but can't change it's value.
Edit: I try to get my value from changeState() into my <input type="text" placeholder="City, Zip Code, Coordinates" onChange={evt => this.updateInputValue(evt)} />. I can search for terms otherwise.
import React from 'react';
import './SearchBar.css';
import Suggestion from './Suggestion';
class SearchBar extends React.Component{
constructor(props) {
super(props);
this.state = {inputValue: ''};
this.search = this.search.bind(this);
this.updateInputValue = this.updateInputValue.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.changeState = this.changeState.bind(this);
}
changeState(value) {
console.log(value);
// Logs value of text between <li></li>
this.setState({inputValue: value});
}
search() {
this.props.onSearch(this.state.inputValue);
}
updateInputValue(evt) {
this.setState({
inputValue: evt.target.value
});
this.props.onChange(this.state.inputValue);
}
handleKeyPress(e) {
if(e.key === 'Enter') {
this.search();
}
}
render() {
return (
<div>
<div className="SearchGroup" onKeyPress={this.handleKeyPress} >
<input type="text" placeholder="City, Zip Code, Coordinates" onChange={evt => this.updateInputValue(evt)} />
<a onClick={this.search}>Go</a>
</div>
<Suggestion autocomplete={this.props.autocomplete} onSelect={this.changeState} />
</div>
);
}
}
export default SearchBar;
For the sake of completeness my Suggestion.js:
import React from 'react';
import './Suggestion.css';
class Suggestion extends React.Component{
constructor(props) {
super(props);
this.updateInputField = this.updateInputField.bind(this);
}
updateInputField(evt) {
this.props.onSelect(evt.currentTarget.innerText);
}
render(){
if(this.props.autocomplete && this.props.autocomplete.length > 0) {
return (
<div className="Suggestion">
<ul>
{
this.props.autocomplete.map((location) => {
return (
<li key={location.id} onClick={this.updateInputField}>{location.name}</li>
)
})
}
</ul>
</div>
);
} else {
return <div className="None"></div>
}
}
}
export default Suggestion;
I would also prefer to submit location.url in Suggestion, but I could not find a property that matches inside of evt.
As mentioned in my comment. You are setting state and immediately passing state to onChange function in updateInputValue event handler function which is not correct. Because you won't get the state value updated immediately, the state value updates only when it renders so, pass evt.target.value directly like below
updateInputValue(evt) {
this.setState({ inputValue: evt.target.value });
this.props.onChange(evt.target.value);
}
In order to see chnaged value on your input field, you have to pass value prop to input tag like below
<input type="text" placeholder="City, Zip Code, Coordinates" onChange={evt => this.updateInputValue(evt)} value={this.state.inputValue}/>
I would guess that you are trying to use value from state that isnt there yet, because setState is asynchronous
so either use callback on setState
updateInputValue(evt) {
this.setState({
inputValue: evt.target.value
}, ()=> this.props.onChange(this.state.inputValue));
}
or, use the value from event directly
updateInputValue(evt) {
const value = evt.target.value
this.setState({
inputValue: value
});
this.props.onChange(value)
}
plus you havent assigned value back to your input:
<input type="text" placeholder="City, Zip Code, Coordinates" onChange={evt => this.updateInputValue(evt)} value={this.state.inputValue}/>
The React setState doesn't update the state immediately. It puts it in the queue and updates the state in batches. if you want to access the updated state write the code in the setState callBack
this.setState({ inputValue: evt.target.value},()=> this.props.onChange(this.state.inputValue));
something like this
I am trying to do two things when the child component's select element is changed-
update child state (used to display more fields and in other child-specific decisions etc.),
call parent handler passed in props.
Only 1st one is working, how do I add 2nd one also? Whats the proper way?
Dummy code:
class Child {
render(){
return(
<div>
//....
<select
id={this.state.searchId}
value={this.state.searchId}
onChange={
//1. event => this.setState({searchId : event.target.value})
//2. call parent props changeHandler
}>
{options}
</select>
//....
</div>
)
}
}
class Parent {
onSearchPrefChange(e) {
this.setState({
searchId : e.target.value
})
}
render(){
<Child onChange={this.onSearchPrefChange} />
}
}
I tried:
onChange={e => {
this.setState({searchId : e.target.value});
this.props.onSearchPrefChange;
}}>
The parent handler is not called, which also is used to change state of parent with same changed element's value/id.
You can define onChange event handler in the child component, set child's set there and call the callback passed from the parent in props from the function. Here is example code:
class Child extends React.Component {
constructor(props) {
super(props);
this.onSelectChange = this.onSelectChange.bind(this);
}
onSelectChange(e) {
const newValue = e.target.value;
this.props.onChange(newValue );
// set child state here
this.setState({searchId : newValue })
}
render() {
return (
<div>
<select onChange={this.onSelectChange}>
<option value="1">1</option>
<option value="2">2</option>
</select>
</div>
)
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.someHandler = this.someHandler.bind(this);
}
someHandler(childValue) {
console.log(childValue);
this.setState({ value: childValue })
}
render() {
return <Child onChange={this.someHandler} />
}
}
Here is a working example on codesandbox.
I making a react app and I have a parent component Search with child components Input and Result. Input has a drop down menu which passes a value, genreValue, to Search, through a callback function when a button is clicked. Search then makes an api call, which works fine.
My problem is it takes two clicks of the button for the new API data to render. Looking at other SO questions I suspect I need to pass genreValue as an argument to the cb function, or my onClick is only initialising, rather than invoking it on the first click.
It's a pretty simple app so I wouldn't think Flux etc would be needed. My console logs seem to show the value being changed in the Search and Input components.
So what am I doing wrong?
Search.js
let Search = React.createClass ({
getInitialState(){
return {
movies: ['Men In Black'],
genreValue: '12'
};
},
componentDidMount(){
this.getMovies()
},
getMovies(){
let genre = this.state.genreValue;
let url = `http://api.themoviedb.org/3/discover/movie?${key}&with_genres=${genre}`;
Request.get(url).then((response) => {
console.log('response.body.results', response.body.results)
this.setState({
movies: response.body.results.map(function(movie){
return movie.title
})
});
});
},
handleGenre(newGenre) {
this.setState({ genreValue: newGenre })
return this.getMovies();
},
render(){
console.log(this.state.movies)
console.log('genreValue state', this.state.genreValue)
return (
<div>
<Input genre={this.state.genreValue} onGenreChanged={this.handleGenre}/>
<ul>
{this.state.movies.map( function(movie){
return <Results key={movie.id} data={movie}/>;
})}
</ul>
</div>
);
}
});
export default Search;
Input.js
let Input = React.createClass ({
selectHandler(){
return this.props.onGenreChanged(this.refs.genre.value);
},
render() {
console.log('genreValue prop', this.props.genre);
console.log('refs', this.refs.genre)
return <div>
<select ref="genre">
<option value="28">Action</option>
<option value="12">Adventure</option>
<option value="16">Animation</option>
<option value="35">Comedy</option>
<option value="80">Crime</option>
<option value="99">Documentary</option>
<option value="18">Drama</option>
<option value="10751">Family</option>
<option value="14">Fantasy</option>
<option value="10769">Non-english</option>
<option value="36">History</option>
</select>
<button onClick={this.selectHandler} value="Go">Go</button>
</div>
}
});
export default Input;
In the handleGenre function, state may not have updated when this.getMovies is called. You could change it to the following:
handleGenre(newGenre) {
this.setState({ genreValue: newGenre }, function() {
return this.getMovies();
});
},
Or, probably better practice would be to call this.getMovies in a componentDidUpdate lifecycle function if genreValue has changed:
componentDidUpdate: function(prevProps, prevState) {
if (prevState.genreValue !== this.state.genreValue) {
this.getMovies();
}
}