I'm mapping over an array of vitamins from json, and I want to return the name of each Vitamin in the array in a dropdown menu when clicked on.
I thought I could declare a const variable in the fetch function, and use that in my JSX.
componentDidMount() {
fetch('/users')
.then(res => res.json())
.then(micros => micros.vitamins.map((micro) => {
const microVit = micro;
}))
}
render() {
return (
<form>
<label className="nutrient-label">
Vitamins
</label>
<select value={this.props.value} onChange={this.handleChange}>
<option value="" selected>--Vitamins--</option>
{microVit.map((vitamin, index) =>
<option value={vitamin.value} key={index}>{vitamin.name}</option>
)}
</select>
</form>
)
}
When I console.log(microVit) in the fetch function, I do get the array of Vitamins. It just doesn't carry over to the map function I'm trying to work in the return statement of my render function.
You need to store it in the state and update it via setState to re-render the component with the new data:
class ... {
state = { microVit: [] }
componentDidMount() {
...
.then(({ vitamins }) => this.setState({ microVit: vitamins })
}
render() {
...
{this.state.microVit.map(...
Your const microVit is encapsulated.
Move the decleration to outside your component did mount method ideally to internal state or a redux store.
Related
I have a search component that fetches a single profile from a JSON file (currently local, but will be remote in the future) and displays the information of the matching profile beneath the input field.
Currently, on my first submit of my search query, I've found that all of my state variables return undefined because, if I understand correctly, state does not update until the full chain of promises has resolved. And it's only on my second submit of my search query that my state variables return the correct data of the filtered search result.
On the first submit, it appears that an empty array is being initialized, as my conditional render of {props.activeChart && `OPENED CHART : ${props.firstName} ${props.lastName} (DOB: ${props.DOB})`} becomes truthy and renders out empty values for firstName, lastName, and DOB.
EDIT: I came across this recent post (React state gets updated only after I submit the form twice), which seems to address this same issue resulting from asynchronous fetch and setting state, except with axios. I've tried modifying my code accordingly (edited below), but I'm still not able to update state after my fetch result has resolved. Any advice would be appreciated. Thanks.
import { useState } from 'react';
import StyledSearchForm from './SearchForm.styled';
const SearchForm = props => {
const [queryFirstName, setQueryFirstName] = useState('');
const [queryLastName, setQueryLastName] = useState('');
const [queryDOB, setQueryDOB] = useState('');
const handleQuery = async (e) => {
e.preventDefault();
const result = await fetchRecord();
console.log(result[0]) // fetched object successfully logged
if (result[0]) {
setActiveChart(result[0]);
console.log(activeChart) // activeChart still undefined
setFirstName(activeChart.firstName);
setLastName(activeChart.lastName);
setDOB(activeChart.dob);
}
};
const fetchRecord = () => (
fetch('http://localhost:8000/patients')
.then(resp => { return resp.json(); })
.then(data => {
const result = data.filter(patient => (
(patient.dob === queryDOB.trim() &&
patient.lastName.toLowerCase() ===
queryLastName.toLowerCase().trim()) ||
(patient.lastName.toLowerCase() ===
queryLastName.toLowerCase().trim() &&
patient.firstName.toLowerCase() ===
queryFirstName.toLowerCase().trim())
));
return {...result};
})
);
return (
<StyledSearchForm>
<form onSubmit={handleQuery}>
<label className="first-name" htmlFor="first-name">
First Name:
</label>
<input
type="text"
id="first-name"
className="form-fields"
name="fname"
value={queryFirstName}
onChange={e => setQueryFirstName(e.target.value)}
/>
<label className="last-name" htmlFor="last-name">
Last Name:
</label>
<input
type="text"
id="last-name"
className="form-fields"
name="lname"
value={queryLastName}
onChange={e => setQueryLastName(e.target.value)}
/>
<label className="dob" htmlFor="dob">
DOB:
</label>
<input
type="text"
id="dob"
className="form-fields"
name="dob"
value={queryDOB}
onChange={e => setQueryDOB(e.target.value)}
/>
<button className="submit-btn" type="submit" onClick={e => handleQuery}>Open Chart</button>
</form>
<div className="active-patient">
{props.activeChart && `OPENED CHART : ${props.firstName} ${props.lastName} (DOB: ${props.DOB})`}
</div>
</StyledSearchForm>
);
};
export default SearchForm;
It looks like you're expecting your data filter to return an object, but Array.prototype.filter (docs) returns an array. Arrays, even if empty, are truthy.
You need to handle an array, not an object, in this chain:
const fetchRecord = () =>
fetch("http://localhost:8000/patients")
.then((resp) => {
return resp.json();
})
.then((data) => {
// results is an array!
const results = data.filter(...);
if (results.length === 0) {
// no match - do something about it?
return {};
} else {
// return the first result?
return results[0];
}
})
.then((record) => {
props.setActiveChart(...record);
})
.then(() => {
props.setFirstName(props.activeChart.firstName);
props.setLastName(props.activeChart.lastName);
props.setDOB(props.activeChart.dob);
});
It seems the issue resulted from trying to set all of my state variables in the same async function that was fetching my search result, and by moving the if(results[0]) statement out of the handleQuery function while leaving just the setActiveChart() inside the handleQuery function resolved my issue.
What I want to do
When state changes, I would like to fetch data from API and set it to state.
Problem
When I load a page, componentDidUpdate is automatically emitted although anything doesn't change and this is initial render.
I am beginner to React.
I would like to get data from API when I input some data and state changes.
However, when I load a page I got an error that cannot property map of undefined.
Apparently, componentDidUpdate is called for some reasons when the page is first rendered.
In fact, I compared prevState.line to this.state.line, and then I found prevState.line undefined.
I mean, componentDidUpdate is emitted before "" that is an initial value of this.state.line is set to this.state.line.
I just would like to prevent componentDidUpdate from being initially called.
If you know a way to do that, I would like you to tell me that and why it is happening.
Thank you very much.
=========== ============ ============
My code is like this.
class User_Add_PickUp_Form extends Component {
constructor(props) {
super(props);
this.state = {
owner: this.props.loginUser,
lines: '',
stations: '',
textInput: '',
allLines: '',
allStations: '',
};
this.handleChange = this.handleChange.bind(this);
}
spreadAndSetState = (key, value) => {
this.setState({ ...this.state, [key]:value });
};
componentDidMount() {
console.log('Original is ' + this.state.lines)
axios
.get('http://express.heartrails.com/api/json?method=getLines&prefecture=TOKYO')
.then((res) => {
console.log(res.data)
this.setState({ allLines: res.data.response.line })
})
.catch((err) => console.log(err));
}
componentDidUpdate(prevState){
console.log(prevState)
console.log(this.state)
if(prevState.lines != this.state.lines){
axios.get('http://express.heartrails.com/api/json?method=getStations&line=' + this.state.lines)
.then((res)=> {
//
// res.data.response.stations is going to be an array
//
res.data.response.station.map((station) => {
this.spreadAndSetState(this.state.allStations, station.name)
})
})
}
}
handleChange = (e) => {
const name = e.target.name;
const value = e.target.value;
this.spreadAndSetState(name,value)
};
render() {
const { owner, lines, stations, textInput, allLines, allStations } = this.state;
if (allLines === '') {
return <CircularProgress />;
} else {
return (
<div>
<h2>Add Pickup Places</h2>
<select name="lines" onChange={this.handleChange}>
<option value="">Choose a line</option>
{allLines.map((line) => {
return <option value={line}>{line}</option>;
})}
</select>
<select name="lines" onChange={this.handleChange}>
<option value="">Choose a station</option>
{allLines.map((line) => {
return <option value={line}>{line}</option>;
})}
</select>
</div>
);
}
}
}
I'm using React Hooks. I set the state property questions after an axios fetch call. Now when I click a button, in its function questions state is still empty
const [questions, setQuestions] = useState([]);
const [customComponent, setCustomComponent] = useState(<div />);
useEffect(() => {
axios.get("urlhere").then(res => {
console.log(12, res.data);
setQuestions(res.data);
res.data.map(q => {
if (q.qualifyingQuestionId == 1) {
setCustomComponent(renderSteps(q, q.qualifyingQuestionId));
}
});
});
}, []);
const handleNext = i => {
console.log(32, questions); //questions is still an empty array here
};
const renderSteps = (step, i) => {
switch (step.controlTypeName) {
case "textbox":
return (
<div key={i}>
<input type="text" placeholder={step.content} />
<button onClick={() => handleNext(i)}>Next</button>
</div>
);
}
};
return <>{customComponent}</>;
Do I need to use reducers here and put the custom component in another "file"?
setQuestions does not update state immediately, you should use the prevState instead to access the new value.
Here's a sandbox to match your codes with some explanation on why it was empty > https://codesandbox.io/s/axios-useeffect-kdgnw
You can also read about it here: Why calling react setState method doesn't mutate the state immediately?
Finally I have my own solution
I passed down the data from the fetch function to another component as props
useEffect(() => {
axios.get('url')
.then((data) => {
setCustomComponent(<Questions questions={data} />)
})
}, [])
I have to create a dropdown with 3-4 options and based on the value selected by the user, I have to then call an API using the selected Value as the search string. The JSON returned from this API should then render as a DataGrid in the mainContent section of the page.
This is how the dropdown looks like, pretty basic:
handleDropdownChange(e) {
this.setState({selectedValue: e.target.value});
}
render() {
<div>
<select id="dropdown" onChange={this.handleDropdownChange} className={classes.mainContent}>
<option value="">select</option>
<option value="option1">1</option>
<option value="Option2">2</option>
<option value="Option3">3</option>
</select>
</div>
And I can then do:
<div>Selected value is : {this.state.selectedValue}</div>
This works!
But instead of the above, I want to use a function something-like:
grabData = () => {
fetch(API + {this.state.selectedValue})
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('something is wrong');
}
})
.then (data => this.setState({ myhits: data.hits, hitsIndex: 0, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
}
}
so, inside the render() I want to basically do- (see that I want to use the selectedValue in the function grabData above):
<div>
Selected value is : {this.grabData}
</div>
I'm open to suggestions on how best to do this. This is what I could think of, but it doesn't work.
Or, even better if someone can help me render that JSON in a datagrid.
I hope I haven't confused everyone :)
If you wish to run grabData, when the user changed the value, then call this function on change.
change this function:
handleDropdownChange(e) {
this.setState({selectedValue: e.target.value}, () => {
// Once the state is updated - call grabData...
this.grabData()
});
}
I marked Gugu's answer as accepted as it did work eventually :-) Thanks bud.
So, I got this to work. This is how:
This is how my dropdown looks like. See I added the grabData() inside my handleDropdownChange():
handleDropdownChange(e) {
this.setState({selectedValue: e.target.value});
this.grabData()
}
grabData = (selectedValue) => {
fetch(API + {selectedValue})
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('something is wrong');
}
})
.then (data => this.setState({ selectedData: data.hits, selectedIndex: 0, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
}
And then in the render() I added a map for the data:
render() {
<div>
<select id="dropdown" onChange={this.handleDropdownChange} className={classes.mainContent}>
<option value="">select</option>
<option value="option1">1</option>
<option value="Option2">2</option>
<option value="Option3">3</option>
</select>
</div>
<div>
<ThemeProvider theme={LIGHT_THEME}>
{
selectedData.map(hit =>
<ThemeProvider theme={DARK_THEME}>
<div key={hit.objectID}>
<button> {hit.created_at} {hit.url}</button>
</div>
</ThemeProvider>
)}
</ThemeProvider>
</div>
I just need to figure out how to render on each click. Right now, it renders on the first selection and doesn't render fresh results if I change the selection.
I'm trying to debounce a component in my webapp. Actually it is a filter for the maxPrice and if the user starts to print, the filter starts to work and all the results disappear until there is a reasonable number behind it.
What I tried so far:
import _ from 'lodash'
class MaxPrice extends Component {
onSet = ({ target: { value }}) => {
if (isNaN(Number(value))) return;
this.setState({ value }, () => {
this.props.updateMaxPrice(value.trim());
});
};
render() {
const updateMaxPrice = _.debounce(e => {
this.onSet(e);
}, 1000);
return (
<div>
<ControlLabel>Preis bis</ControlLabel><br />
<FormControl type="text" className={utilStyles.fullWidth} placeholder="egal"
onChange={updateMaxPrice} value={this.props.maxPrice}
/>
</div>
);
}
}
I'm getting the error
MaxPrice.js:11 Uncaught TypeError: Cannot read property 'value' of null
at MaxPrice._this.onSet (MaxPrice.js:11)
at MaxPrice.js:21
at invokeFunc (lodash.js:10350)
at trailingEdge (lodash.js:10397)
at timerExpired (lodash.js:10385)
In my old version I had onChange={this.onSet} and it worked.
Any idea what might be wrong?
As you mentioned in comments, it's required to use event.persist() to use event object in async way:
https://facebook.github.io/react/docs/events.html
If you want to access the event properties in an asynchronous way, you
should call event.persist() on the event, which will remove the
synthetic event from the pool and allow references to the event to be
retained by user code.
It means such code, for example:
onChange={e => {
e.persist();
updateMaxPrice(e);
}}
Here is my final solution. Thanks to lunochkin!
I had to introduce a second redux variable so that the user see's the values he is entering. The second variable is debounced so that the WepApp waits a bit to update.
class MaxPrice extends Component {
updateMaxPriceRedux = _.debounce((value) => { // this can also dispatch a redux action
this.props.updateMaxPrice(value);
}, 500);
onSet = ({ target: { value }}) => {
console.log(value);
if (isNaN(Number(value))) return;
this.props.updateInternalMaxPrice(value.trim());
this.updateMaxPriceRedux(value.trim());
};
render() {
return (
<div>
<ControlLabel>Preis bis</ControlLabel><br />
<FormControl type="text" className={utilStyles.fullWidth} placeholder="egal"
onChange={e => {
e.persist();
this.onSet(e);
}} value={this.props.internalMaxPrice}
/>
</div>
);
}
}
function mapStateToProps(state) {
return {
maxPrice: state.maxPrice,
internalMaxPrice: state.internalMaxPrice
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({updateMaxPrice:updateMaxPrice,
updateInternalMaxPrice:updateInternalMaxPrice}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(MaxPrice);