ReactJS : How to properly handle data fetched from food2fork API? - reactjs

I have an app built with ReactJS. Its purpose is to display recipes, searched in food2fork API.
I have no problems with updating state of parent component. Data is fetched after clicking 'search' button in app.
My issue is related with sending fetched data as props to child component and properly displaying received recipes based on current search.
handleChange is only for handling input.
handleSearch is what I wanted to use 'onClick' of a button to display data fetched from API.
Fetched recipes should be displayed in Results component.
Hope it is clear :)
Besides only passing state as props from Parent component and using it in Child component, I also tried to update Child state based on received props with lifecycle methods - maybe I haven't used them corrently ...
Parent component:
import React, { Component } from 'react';
import Results from './Results';
class Recipes extends Component {
constructor(props){
super(props);
this.state = {
search: '',
recipes: []
}
}
handleChange=e=>{
this.setState({
search: e.target.value
})
}
handleSearch =()=>{
if(this.state.search !== ''){
const url = `https://www.food2fork.com/api/search?key=367d2d744696f9edff53ec5b33a1ce64&q=${this.state.search}`
fetch(url)
.then(data => data.json())
.then(jsonData => {
this.setState((jsonData)=> {return {
recipes: jsonData}
})
})
} else {
console.log('empty')
}
}
render() {
return (
<Wrapper>
<SearchBar
value={this.state.search}
type='search'
onChange={this.handleChange}>
</SearchBar>
<SearchButton onClick={this.handleSearch}>SEARCH</SearchButton>
<Results recipes={this.state.search}/>
</Wrapper>
);
}
}
export default Recipes;
CHILD COMPONENT 'Results' which should receive updated recipe list as props and display these recipes.
import React from 'react';
import Recipe from './Recipe';
class Results extends React.Component {
render(){
return (
<Container>
<RecipesList>
{this.props.recipes.map(item =>
<Recipe
f2fURL={item.f2f_url}
image={item.image_url}
publisher={item.publisher}
publisherURL={item.publisher_url}
recipeID={item.recipe_id}
source={item.source_url}
title={item.title}
/>)}
</RecipesList>
</Container>
);
}
};

As #yourfavoritedev mentioned, you have a typo on Results props. It should be
recipes={this.state.recipes} instead of recipes={this.state.search}
You should also change:
this.setState((jsonData)=> {return {
recipes: jsonData}
})
for:
this.setState({ recipes: jsonData })
The updater function will be something like this (documentation here):
(state, props) => stateChange
So the jsonData you are using on your setState is actually the previous state and not the data coming from the api call.

Your problem is here
this.setState((jsonData)=> {return {
recipes: jsonData}
})
inside your ajax response.
Change this to
this.setState({recipes: jsonData});
This should set the recipes object correctly.

Related

Send Data From Parent Component To Child Component In React

I am new to react & need some help. I am getting data from a REST API using Axios. I have two Components. A Parent & Child Component. In parent Component I am fetching the Summarised data from API having multiple records while the Child component is used to make another API call for Details of the record when user clicks on a specific record in the Parent Component.
The Parent Component has 3 attribute ( Document-Number, document-Type & Approver ). I need to pass the "Doc-Number" & " Doc-Type" values to the child Component API URl when user clicks on the button.
Note: I donot have any dedicated ID attribute in the Parent API response and that's the reason I am using index as a key.
Here is My Parent Component
import React, { Component } from "react";
import axios from "axios";
import Getdetails from "./Getdetails";
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
records: [],
errorMessage: "",
};
}
componentDidMount() {
axios
.get( "http://www.example.Api.com/generalInfo&limit=10&offset=2" )
.then((res) => {
this.setState({ records: res.data });
console.log(res);
})
}
render() {
const { records } = this.state;
return (
<div>
<ul>
{records.map((record, index) => (
<li key={index}>
Document Number : {record.Number}
Document Type: {record.documentType}
Approver : {record.approver}
//I Want to send the "document Number & documentType" to Childdetails component Url when user click on this button.
<button onClick={{record.Number} & {record.documentType}}>Get Details</button>
</li>
))}
</ul>
</div>
)
}
}
export default Parent;
Here is My Child Component
import React, { Component } from "react";
import axios from "axios";
import Parent from "Parent";
class ChildDetails extends Component {
constructor(props) {
super(props);
this.state = {
getdetails: [],
errorMessage: "",
};
}
componentDidMount() {
axios
.get("http://www.example-Details-API.com/documentType={record.documentType}/id={record.Number}")
.then((res) => {
this.setState({ getdetails: res.data });
console.log(res);
})
}
render() {
const { getdetails } = this.state;
return (
<div>
<h1>Get Details</h1>
<ul>
<li> Number : {getdetails.Number} </li>
<li> Title : {getdetails.Title} </li>
<li> Submit Date : {getdetails.Date} </li>
<li> Site : {getdetails.site} </li>
<li> Requester : {getdetails.requesterName}</li>
<li> document Type : {getdetails.documentType}</li>
</ul>
</div>
)
}
}
export default ChildDetails
Thanks To everyone and Your help is really appreciated.
When you talk about Parent and Child components I expect to see the Child rendered by the Parent, I am not sure if this is your case. Anyway, the main way to pass data from parents to childs are via the props. Applied to your example:
In the parent's render function:
<ChildDetails record={record} />
In the child's render function:
componentDidMount() {
axios
.get(`http://www.example-Details-API.com/documentType=${props.record.documentType}/id=${props.record.Number}`)
.then((res) => {
this.setState({ getdetails: res.data });
console.log(res);
})
}
See that in the child the data is accessed via props.record.
If your ChildDetails is not rendered by the Parent, then you need to pass the information to upper levels through callbacks.
Passing data as a prop to child component
const onClickHandler = (record,document) => {
return (
<ChildDetails recordNumber={record} documentType={document}/>
)
};
Passing data as parameters to event handler
<button onClick={onClickHanlder(record.Number,record.documentType)}>Get Details</button>
If you wannna use index you can use it as a third argument
You can add two more state values in your parent component such as:
this.state = {
records: [],
errorMessage: "",
Selected-Doc-Number: ""
Selected-Doc-Type: ""
};
Now on you can set these state values (Selected-Doc-Number, Selected-Doc-Type) on the click of record button on parent component as:
const selectRecordForChildComponent = (selectedDocNumber, selectedDocType) => {
this.setState({Selected-Doc-Number: selectedDocNumber,
Selected-Doc-Type: selectedDocType})
}
<button
onClick={() => {selectRecordForChildComponent(record.Number, record.documentType)}}>
Get Details
</button>
Now on you can pass these values (Selected-Doc-Number, Selected-Doc-Type) to child component using props as from the Parent-component:
<ChildDetails
selectDocNumber = {this.state.Selected-Doc-Number}
selectedDocType = {this.state.Selected-Doc-Type} />
Now you can access these passed props in <ChildDetails> component using it's props as for example:
componentDidMount() {
const docNumber = this.props.selectDocNumber
const docType = this.props.selectedDocType
axios
.get(`http://www.example-Details-API.com/documentType=${docType}/id=${docNumber}`)
.then((res) => {
this.setState({ getdetails: res.data });
console.log(res);
})
}
Hope this may help...
In the parent component onClick create a function that will be called and return the props to the child component.
<button onClick={() => this.handleClick(record.number, record.documentType)}>Get Details</button>
And the handle click function should be like that
handleClick(num, type) {
return (
<Child recordNum={num} docType={type}></Child>
)
};
Don't forget to bind the function in the constructor. You can then call the external API in the did mount function in the child and replace the url with the required props from parent like in the above example this.props.recordNum and this.props.docType.

Dispatching Redux actions on location change with react-router-dom

I am using React and Redux for a search application. Using react-router-dom, I'm routing /search/:term? to a Search component:
<Router>
<Switch>
<Route exact path="/search/:term?" component={Search} />
<Redirect to="/search" />
</Switch>
const Search = (props) => {
const { term } = props.match.params;
return (
<div>
<SearchForm term={term}/>
<SearchResults />
</div>
)
};
When a user submits a search in the SearchForm component, I'm dispatching an action to submit the search query. I'm also initiating a search in the constructor if a term is given, initially:
class SearchForm extends Component {
constructor(props) {
super(props);
const term = props.term ? props.term : '';
this.state = {
term: term,
}
if (term) {
this.props.submitSearch(term);
}
}
handleSubmit = (e) => {
e.preventDefault();
if (this.state.term) {
this.props.submitSearch(this.state.term);
}
}
render = () => {
<form
onSubmit={this.handleSubmit.bind(this)}>
...
</form>
}
}
I'm using withRouter from react-router-dom, so the URL updates when the search is submitted.
The problem happens when the user navigates Back in their browser. The URL navigates back, the props update (i.e. props.match.params.term), but the search does not resubmit. This is because the submitSearch action only gets dispatched in SearchForm.constructor (search on initial loading if a term is in the URL) and SearchForm.handleSubmit.
What is the best way to listen for a state change to term when the URL changes, then dispatch the search action?
I would retrieve the route parameter in componentDidMount since you are pushing a new route and therefore reloading the view.
Inside your SearchForm it would look like this.
state = {
term: '';
}
onChange = (term) => this.setState({ term })
onSubmit = () => this.props.history.push(`/search/${this.state.term}`);
And in your SearchResult :
componentDidMount() {
this.props.fetchResults(this.props.term)
}
A nice thing to do would be to keep the SearchResult component dry. There are several ways to achieve that, here is one using higher order components aka HOC :
export default FetchResultsHoc(Component) => {
#connect(state => ({ results: state.searchResults }))
class FetchResults extends React.Component {
componentDidMount(){
dispatch(fetchResults(this.props.match.params.term))
}
render(){
<Component {...this.props} />
}
}
return FetchResultsHoc;
}
That you would then call on your SearchResult component using a decorator.
import { fetchResults } from './FetchResultsHoc';
#fetchResults
export default class SearchResult extends React.PureComponent { ... }
// You have now access to this.props.results inside your class
My current solution is to dispatch submitSearch in the componentWillRecieveProps lifecycle method if the new props don't match the current props:
componentWillReceiveProps(nextProps) {
if (this.props.term !== nextProps.term) {
this.setState({
term: nextProps.term,
});
this.props.submitSearch(nextProps.term);
}
}
Then, instead of dispatching an action on form submission, I push a new location onto the history and componentWillReceiveProps does the dispatching:
handleSubmit = (e) => {
e.preventDefault();
if (this.state.term) {
this.props.history.push('/search/'+this.state.term);
}
}
This solution feels a little wrong, but it works. (Other's would seem to agree: Evil things you do with redux — dispatch in updating lifecycle methods)
Am I violating a React or Redux principle by doing this? Can I do better?

What is best approach to set data to component from API in React JS

We have product detail page which contains multiple component in single page.
Product Component looks like:
class Product extends Component {
render() {
return (
<div>
<Searchbar/>
<Gallery/>
<Video/>
<Details/>
<Contact/>
<SimilarProd/>
<OtherProd/>
</div>
);
}
}
Here we have 3 APIs for
- Details
- Similar Product
- Other Products
Now from Detail API we need to set data to these components
<Gallery/>
<Video/>
<Details/>
<Contact/>
In which component we need to make a call to API and how to set data to other components. Lets say we need to assign a,b,c,d value to each component
componentWillMount(props) {
fetch('/deatail.json').then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong ...');
}
})
.then(data => this.setState({ data, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
}
OR
Do we need to create separate api for each components?
Since it's three different components you need to make the call in the component where all the components meet. And pass down the state from the parent component to child components. If your app is dynamic then you should use "Redux" or "MobX" for state management. I personally advise you to use Redux
class ParentComponent extends React.PureComponent {
constructor (props) {
super(props);
this.state = {
gallery: '',
similarPdts: '',
otherPdts: ''
}
}
componentWillMount () {
//make api call and set data
}
render () {
//render your all components
}
}
The Product component is the best place to place your API call because it's the common ancestor for all the components that need that data.
I'd recommend that you move the actual call out of the component, and into a common place with all API calls.
Anyways, something like this is what you're looking for:
import React from "react";
import { render } from "react-dom";
import {
SearchBar,
Gallery,
Video,
Details,
Contact,
SimilarProd,
OtherProd
} from "./components/components";
class Product extends React.Component {
constructor(props) {
super(props);
// Set default values for state
this.state = {
data: {
a: 1,
b: 2,
c: 3,
d: 4
},
error: null,
isLoading: true
};
}
componentWillMount() {
this.loadData();
}
loadData() {
fetch('/detail.json')
.then(response => {
// if (response.ok) {
// return response.json();
// } else {
// throw new Error('Something went wrong ...');
// }
return Promise.resolve({
a: 5,
b: 6,
c: 7,
d: 8
});
})
.then(data => this.setState({ data, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
}
render() {
if (this.state.error) return <h1>Error</h1>;
if (this.state.isLoading) return <h1>Loading</h1>;
const data = this.state.data;
return (
<div>
<SearchBar/>
<Gallery a={data.a} b={data.b} c={data.c} d={data.d} />
<Video a={data.a} b={data.b} c={data.c} d={data.d} />
<Details a={data.a} b={data.b} c={data.c} d={data.d} />
<Contact a={data.a} b={data.b} c={data.c} d={data.d} />
<SimilarProd/>
<OtherProd/>
</div>
);
}
}
render(<Product />, document.getElementById("root"));
Working example here:
https://codesandbox.io/s/ymj07k6jrv
You API calls will be in the product component. Catering your need to best practices, I want to make sure that you are using an implementation of FLUX architecture for data flow. If not do visit phrontend
You should send you API calls in componentWillMount() having your state a loading indicator that will render a loader till the data is not fetched.
Each of your Components should be watching the state for their respective data. Let say you have a state like {loading:true, galleryData:{}, details:{}, simProducts:{}, otherProducts:{}}. In render the similar products component should render if it finds the respective data in state. What you have to do is to just update the state whenever you receive the data.
Here is the working code snippet:
ProductComponent:
import React from 'react';
import SampleStore from '/storepath/SampleStore';
export default class ParentComponent extends React.Component {
constructor (props) {
super(props);
this.state = {
loading:true,
}
}
componentWillMount () {
//Bind Store or network callback function
this.handleResponse = this.handleResponse
//API call here.
}
handleResponse(response){
// check Response Validity and update state
// if you have multiple APIs so you can have a API request identifier that will tell you which data to expect.
if(response.err){
//retry or show error message
}else{
this.state.loading = false;
//set data here in state either for similar products or other products and just call setState(this.state)
this.state.similarProducts = response.data.simProds;
this.setState(this.state);
}
}
render () {
return(
<div>
{this.state.loading} ? <LoaderComponent/> :
<div>
<Searchbar/>
<Gallery/>
<Video/>
<Details/>
<Contact/>
{this.state.similarProducts && <SimilarProd data={this.state.similarProducts}/>}
{this.state.otherProducts && <OtherProd data={this.state.otherProducts}/>}
</div>
</div>
);
}
}
Just keep on setting the data in the state as soon as you are receiving it and render you components should be state aware.
In which component we need to make a call to API and how to set data
to other components.
The API call should be made in the Product component as explained in the other answers.Now for setting up data considering you need to make 3 API calls(Details, Similar Product, Other Products) what you can do is execute the below logic in componentDidMount() :
var apiRequest1 = fetch('/detail.json').then((response) => {
this.setState({detailData: response.json()})
return response.json();
});
var apiRequest2 = fetch('/similarProduct.json').then((response) => { //The endpoint I am just faking it
this.setState({similarProductData: response.json()})
return response.json();
});
var apiRequest3 = fetch('/otherProduct.json').then((response) => { //Same here
this.setState({otherProductData: response.json()})
return response.json();
});
Promise.all([apiRequest1,apiRequest2, apiRequest3]).then((data) => {
console.log(data) //It will be an array of response
//You can set the state here too.
});
Another shorter way will be:
const urls = ['details.json', 'similarProducts.json', 'otherProducts.json'];
// separate function to make code more clear
const grabContent = url => fetch(url).then(res => res.json())
Promise.all(urls.map(grabContent)).then((response) => {
this.setState({detailData: response[0]})
this.setState({similarProductData: response[1]})
this.setState({otherProductData: response[2]})
});
And then in your Product render() funtion you can pass the API data as
class Product extends Component {
render() {
return (
<div>
<Searchbar/>
<Gallery/>
<Video/>
<Details details={this.state.detailData}/>
<Contact/>
<SimilarProd similar={this.state.similarProductData}/>
<OtherProd other={this.state.otherProductData}/>
</div>
);
}
}
And in the respective component you can access the data as :
this.props.details //Considering in details component.

When I set component's state with concat I receive error: Warning: Each child in an array or iterator should have a unique "key" prop.

My React component renders a twitter stream. The original version works correctly, but the componentDidMount method mutates the component's state:
(Original) components/Stream.js
import React, { Component } from 'react';
import { Button } from 'react-bootstrap';
class Stream extends Component {
constructor() {
super();
this.state = { streamItems: [] }
}
componentDidMount() {
fetch('/tweets')
.then(res => res.json())
.then(tweets => this.setState({ streamItems: tweets }));
}
render() {
return (
<div>
<h1>Tweets</h1>
<div className='stream-items'>
{this.state.streamItems.map(tweet =>
<div key={tweet.id}>{tweet.text}</div>
)}
</div>
<Button
className='btn-remove'
onClick={() => this.props.removeStream(this.props.stream.id)}
>
Remove Stream
</ Button>
</div>
)
}
}
export default Stream;
To prevent the component's state being mutated I setState using concat instead:
components/Stream.js (updated method only)
componentDidMount() {
fetch('/tweets')
.then(res => res.json())
.then(tweets => this.setState({
streamItems: this.state.streamItems.concat([tweets])
}))
}
I now receive the error:
Warning: Each child in an array or iterator should have a unique
"key" prop.
This is confusing as I have set the key in the component's render method.
Warning: Each child in an array or iterator should have a unique "key" prop.
streamItems: this.state.streamItems.concat([tweets])
Its look like tweets is not having id.Its coming undefined.So,React throwing an error. Or you need to spread it
streamItems: this.state.streamItems.concat([...tweets])
Note:
You don't need to use concat.You can do like this.
streamItems:[...this.state.streamItems,tweets]

Re-rendering react-redux component

I am using react-redux code structure and this is my first try with react-redux.I have cloned a github repository from Here and started editing it.
My directory structure:
Here schema is parent component and header and tables are 2 child components.Tables is showing data from localstorage through redux store.
Initializing store:
const initialState = JSON.parse(window.localStorage.getItem('test'));
const store = createStore(Reducers, initialState, compose(applyMiddleware(...middleware), extension));
Now an event is triggered from Header and sent to schema and in the response of this event schema is updating localstorage by requesting to server and saving server's response in localstorage as follows:
Schema.js:
class Schema extends PureComponent {
constructor(props) {
super(props);
this.table = '';
getTables();
}
myCallback = () => {//triggered by child(Header)
getTables();
}
getTables = () => {
axios.get(url)
.then((response) => {
if(response.data.status==0){
window.localStorage.setItem('test',JSON.stringify(response.data));
this.tables=JSON.parse(window.localStorage.getItem('test'))
});
}
render() {
console.log(this.tables);//this is giving updated value at each updation
return (
<div className='container-fluid'>
<Header callbackFromParent={ this.myCallback } />
<br />
<br />
<Tables val={ this.tables } />
</div>
);
}
}
Here is code for Tables.js:
class Tables extends Component {
props: Props
render() {
let {tables,val } = this.props;
console.log(JSON.parse(window.localStorage.getItem('test')));//this is giving updated value at each updation in localstorage
console.log(val);//this is also giving updated value at each updation in localstorage
tables=val;
console.log(tables);this is also updating in realtime.
return (
<div className='table-wrapper'>
{ tables.map((table) => (
<Table
key={ table.id }
data={ table }
/>
))}
</div>
);
}
}
type Props = {
tables: Array<TableType>
};
The issue is whenever header triggers callback, schema updates value of localstorage, this updation also re render Tables component. Also an updated value can be seen in render of Tables component but the tables which are shown are from previous saved value. To get current value in tables, we need to refresh the page.
is it a mistake in code flow or i need something else for this?
The idea is that react will trigger rendering of component whenever the component state or props is updated.
If the component props are updated in parent component you will still need to update the component state to make the next render in inner component
The key of this is using componentWillReceiveProps
I updated your code with the code below:
Basically i did the following:
1- I used component state for Schema, Tables, and Table
2- I used this.setState whenever i need to make updates to state (this is very important)
3- I make sure that when a component props is updated in parent i update the component state as well using componentWillReceiveProps and this will make the new render with updated data
Schema component:
class Schema extends Component {
constructor(props) {
super(props);
this.state = { tables : { } }
this.getTables = this.getTables.bind(this);
this.myCallback = this.myCallback.bind(this);
}
componentWillMount(){
this.getTables();
}
myCallback = () => {
//triggered by child(Header)
this.getTables();
}
getTables = () => {
axios.get(url)
.then((response) => {
if(response.data.status==0)
{
window.localStorage.setItem('test',JSON.stringify(response.data));
this.setState({
tables : JSON.parse(window.localStorage.getItem('test'))
});
}
);
}
render() {
//this is giving updated value at each updation
console.log(this.state.tables);
return (
<div className='container-fluid'>
<Header callbackFromParent={ this.myCallback } />
<br />
<br />
<Tables tables={this.state.tables} />
</div>
);
}
}
Tables Component
class Tables extends Component {
constructor(props) {
super(props);
this.state = { tables : { } }
}
componentWillMount(){
this.setState({
tables : this.props.tables
})
}
componentWillReceiveProps(nextProps){
this.setState({
tables : nextProps.tables
})
}
render() {
console.log(JSON.parse(window.localStorage.getItem('test')));//this is giving updated value at each updation in localstorage
console.log(this.state.tables);//this is also giving updated value at each updation in localstorage
return (
<div className='table-wrapper'>
{ this.state.tables.map((table) => (
<Table key={ table.id } data={ table } />
))
}
</div>
);
}
}
And finally a dummy Table component to show that you will also need to handle props update using componentWillReceiveProps to make sure each individual table component did render after props update
And probably this is where you have the issue ... because the tables are showing old data but the console.log of Tables component is logging new data which means Each individual Table component is not rending after the update
class Table extends Component {
constructor(props) {
super(props);
this.state = { data : { } }
}
componentWillMount(){
this.setState({
data : this.props.data
})
}
componentWillReceiveProps(nextProps){
this.setState({
data : nextProps.data
})
}
render() {
console.log(this.state.data);
return (
<table className='table'>
{this.state.data}
//use your table data here
</table>
);
}
}
Important Edit:
As mentioned by react documentation componentWillReceiveProps might get called even if the props have not changed, thats why in some situation you might need to consider comparing this.props with nextProps to make sure that you really got new updated props and based on that you update the component state ....

Resources