displaying data from fetch api using react - reactjs

I am developing a simple website which displays data from an API(JSON) into a page using react.
I'm using the fetch() API.
I am able to get the data from the API and set it into the 'App' component state but I'm not able to pass is down to Table and Row components that I've manually created.
class App extends React.Component {
constructor (props) {
super(props)
this.state = {ticker: {}, volume: {}}
this.loadData = this.loadData.bind(this)
this.loadData()
}
loadData () {
fetch(ticker)
.then((resp) => resp.json())
.then((data) => {
this.setState({
ticker: data
})
})
.catch((err) => {
console.log(err)
})
fetch(volume)
.then((resp) => resp.json())
.then((data) => {
this.setState({
volume: data
})
})
.catch((err) => {
console.log(err)
})
}
render () {
return (
<div>
<Navbar />
<div className='container'>
<div className='align'>
<div className='element' />
</div>
<Table volume={this.state.volume} ticker={this.state.ticker} />
</div>
</div>
)
}
}
BOTTOM LINE:
I have an API with data, and I have 3 components, Table, which also has a row component.
I want to display variables in the Row component
which looks like this
<Row img='eth' name='Ethereum' price='' volume='' change='' marketCap='' />

You constructor :
constructor (props) {
super(props);
this.state = {ticker: {}, volume: {}}
this.loadData = this.loadData.bind(this);
}
For fetching data you need always to use lifecycle component, as componentDidMount or componentWillMount, thus :
componentDidMount(){
this.loadData()
}
And then in your state you will have the data.
In your render method pass it as props to the Table component:
render(){
return(
<Table volume={this.state.volume} ticker={this.state.ticker} />
)
}
Then in from the Table component pass it to Row component as props, thus :
render(){
return(
<Row img='eth' name='Ethereum' price='' volume={this.props.volume} change='' marketCap='' />
)
}
If you have array of objects, something like :
this.state = {
volume: [ {name: "One", size: 1 }, {name: "Two", size: 2 }, ..... ]
}
You'll need to loop through the array and show the Row component for each object.
Thus, your Table component should be something as follows:
render(){
return (
<div>{this.props.volume.map(vol => <Row img='eth' name='Ethereum' price='' volume={vol} change='' marketCap='' />) }</div>
)
}

If you make the ajax call in componentDidMount, then React will rerender when the state changes (https://facebook.github.io/react/docs/react-component.html#componentdidmount). But you still have to anticipate that the volume and ticker props will be empty until the request resolves and React rerenders.

Related

Map doesn't work in render after fetch in React JS

I'm struggling with fetching data and render to the screen in React JS
class Home extends Component{
componentWillMount(){
foods=[];
fetch('http://192.249.19.243:0280/main/get_recipes')
.then(res => res.json())
.then(data => foodlist=data)
.then(
() => console.log("f:",foodlist),
)
.then(
() => {foodlist.map(item => foods.push({title:item, img:"http://192.249.19.243:0280/main/image/"+item}));
console.log("foods", foods);
this.render();
}
);
}
componentDidMount(){
}
render(){
console.log("render in!");
return (
<div>
<ul>
{
console.log(foods), // this works fine -> 4 elements
foods.length!=0 ?
foods.map(item=>
<Item
title={item.title}
img={item.img}/>
)
:
<p id="loadingMsg">Data Loading...</p>
}
</ul>
</div>
);
}
}
export default Home;
in the render(), I checked console.log(foods) print 4 elements,
but Nothing appears in the screen..
I don't know why.. Please help me..
In react: it is not you who manage the render. If you want to render an element you need to call this.setState with the data that changed. You can see my example:
class Home extends Component{
state = {
foods: []
}
componentWillMount(){
fetch('http://192.249.19.243:0280/main/get_recipes')
.then(res => res.json())
.then(data => foodlist=data)
.then(
() => console.log("f:",foodlist),
)
.then(
() => {
this.setState({foods: foodlist.map(item => ({title:item, img:"http://192.249.19.243:0280/main/image/"+item})));
}
);
}
componentDidMount(){
}
render(){
console.log("render in!");
return (
<div>
<ul>
{
this.state.foods.length!=0 ?
this.state.foods.map(item=>
<Item
title={item.title}
img={item.img}/>
)
:
<p id="loadingMsg">Data Loading...</p>
}
</ul>
</div>
);
}
}
export default Home;
It looks like you are relatively new to React. I spot quite a few errors with this.
Please read the docs on class based components carefully
I have tried to refactor it without context. Give it a bash
class Home extends Component {
//initialize state in the constructor for class based components
constructor(props) {
super(props);
//foods must be an empty array otherwise .length may fail
this.state = { foods: [] }
};
//once the component has mounted, call the method which will perform the fetch
componentDidMount() {
this.fetchFoodData();
}
//calls the endpoint which returns a promise. The promise will then set the components state, which will trigger a render
fetchFoodData = () => {
fetch('http://192.249.19.243:0280/main/get_recipes')
.then(res => {
const foodData = res.json();
//not sure what your body looks like, but foods should be an array containing your food objects
const foods = foodData.map(item => foods.push({ title: item, img: "http://192.249.19.243:0280/main/image/" + item}));
//calling setState will cause react to call the render method.
this.setState({ foods: foods })
}).catch(err => {
//handle errors here
console.log(err);
});
};
//React calls this method when props or state change for this component
render() {
return (
<div>
<ul>
{
foods.length != 0 ?
foods.map(item =>
<Item
title={item.title}
img={item.img} />
)
:
<p id="loadingMsg">Data Loading...</p>
}
</ul>
</div>
);
}
}
export default Home;
Thats not the correct way to handle data in a react component. You should maintain list of foods in component state. Code sandbox: https://codesandbox.io/s/falling-bush-b9b78
As an example
import React from "react";
export default class App extends React.Component {
constructor() {
super();
this.state = {
foods: []
};
}
componentDidMount() {
const fetchMock = url =>
new Promise(resolve => {
setTimeout(() => resolve(["Barley", "Chicken", "Oats"]), 2000);
});
fetchMock("http://192.249.19.243:0280/main/get_recipes").then(foods => {
this.setState({
foods
});
});
}
render() {
console.log("render in!");
const { foods } = this.state;
return (
<div>
<ul>
{foods.length !== 0 ? (
foods.map(food => <h1 key={food}>{food}</h1>)
) : (
<p id="loadingMsg">Data Loading...</p>
)}
</ul>
</div>
);
}
}

How to update state of parent from child component in reactJS

class A extends Component {
constructor(props) {
super(props);
this.state = {
fruitsDetailsList: [],
fruitCode: this.props.fruitCode,
};
}
showModal = () => {
this.setState({ show: true });
};
hideModal = () => {
this.setState({ show: false });
};
componentDidMount() {
const url = 'http://localhost:3000/getFruitslist';
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fruitCode: this.state.fruitCode,
}),
})
.then(res => {
if (res.ok) {
return res.json();
}
throw new Error(res.status);
})
.then(res => {
this.setState({
fruitsDetailsList: res.fruitDetailsList,
});
})
.catch(error => {});
}
render() {
const columns = [
{
Header: 'Sr.No',
id: "row",
maxWidth: 50,
Cell: (row) => {
return <div>{row.index + 1}</div>
}
},
{
Header: 'Actions',
id: 'FruitName',
accessor: d => (
<div>
<B/>
</div>
)
}
];
return (
<div>
<ReactTable
className="-striped -highlight"
columns={columns}
data={this.state.fruitsDetailsList}
defaultPageSize={10}
noDataText={'No Data available.'}
/>
<p></p>
</div>
);
}
Class B extends component{
constructor(props) {
super(props);
this.state = { modal: false };
this.toggle = this.toggle.bind(this);
}
toggle() {
this.setState({
modal: !this.state.modal
});
}
render() {
return (
<div>
<Button onClick={this.toggle}/>
<Modal isOpen={this.state.modal}>
<ModalHeader>Fruits list</ModalHeader>
<ModalBody>
<Formik
initialValues={{fruitName: ''}}
onSubmit={(fields, action) => {
action.setSubmitting(true);
const url = 'http://localhost:3000/getFruit';
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fruitName: fields.fruitName,
}),
})
.then(res => {
action.setSubmitting(false);
console.log("Success!!!);
})
.catch(error => {});
}}
render={({ errors, touched, isSubmitting }) => (
!isSubmitting ? (
<Form>
<div className="form-group">
<label htmlFor="fruitName">FruitName</label>
<Field name="fruitName" type="text"/>
</div>
<div className="form-group">
<Button type="submit" className="bg-gradient-theme-left border-0 " size="m">Submit</Button>
</div>
</Form>
)
)}
/>
</ModalBody>
</Modal>
</div>
);
}
}
- REACT JS
As you can see there are 2 classes
1)Component A
2)Component B
In Component A I am calling B component as Button in react-table
Actually we have to render react table with all the data in database by calling post API request '/getFruitslist' which we are calling in componentDidMount of Component A so that data in react-table gets populated correctly in table
Now when we click on button of Component B one record gets inserted in
database but as API is called in ComponentDidMount of Component A which is
parent of Component B , data does not populate in react-table on clicking button of B component . How to achieve this ?
The philosophy of React is to manage the state into the top component. A child component should not keep a reference to its parent in order to update the parent state.
To achieve this behavior, the parent component should pass a callback to the child component through its properties, then the child component can invoke this callback by calling the matching property
In your example, you can pass a property onAddFruit to the B component. When the POST call succeed, you should call this function
.then(fruit => {
action.setSubmitting(false);
console.log("Success!!!);
this.props.onAddFruit(fruit);
})
In the parent component A, you define a function which will add a fruit to the state variable fruitsDetailsList. Then you pass this function to the component B through the property onAddFruit
handleAddFruit = fruit => {
this.setState(prevState => ({
fruitsDetailsList: [...prevState.fruitsDetailsList, fruit]
});
};
The declaration of the B element will be like this :
<B onAddFruit={this.handleAddFruit}/>
There are ways to achieve what you are looking for. The easiest one is to pass down through props the necessary objects, values. In complex cases I would recommend to use redux for application state management, it is way more easier to keep your state consistent in a case when you need to have the state objects accessible in several components.
Pass down the props:
Once you have 2 components, data can be passed down for example from A to B. Sharing below 1 small example how you can achieve that:
class A extends Component {
constructor(props) {
super(props);
this.state = {
fruitsDetailsList: [],
fruitCode: this.props.fruitCode,
};
}
render {
return (
<B dataFromA={this.state.fruitsDetailsList} />
)
}
}
Redux option:
So what is Redux?
A predictable state container for JavaScript apps.
Basically your application will have the state object in 1 common place what you can match to each components if it needs to be accessed with mapStateToProps function from redux. The link Connect: Extracting Data with mapStateToProps explains how to that in more details. Once you need to modify any of the elements you can use mapDispatchToProps to do so. Please find the Connect: Dispatching Actions with mapDispatchToProps link for further setup.
Read further about the setup, getting started here: Redux
Summary:
In your scenario because of API calls, possible frequent update in state objects I recommend to go with the second option. It might be complicated at the first look but it is worth to effort because you don't need to worry about the state consistency and through props the data is updated automatically once you have modification on any of the objects.
I hope this helps!

What is the correct way to call API inside a Modal, when it's visible in React?

Suppose that I have a Modal inside a Component and I want to show the Modal when some buttons are clicked:
render(){
...
<Modal
is={this.state.productId}
visilble={this.state.visible}
/>
}
Inside the Modal Component, I want to call API to get the product detail based on the selected id as the following:
componentWillReceiveProps(nextProps) {
if(nextProps.visible && !this.props.visible) {
fetch(...).then(res => {
this.setState({
product: res.data
})
}).catch(err => {
...
})
}
}
From the React docs it says that componentWillReceiveProps, componentWillUpdate is deprecated and you should avoid them in new code.So I try use static getDerivedStateFromProps()
static getDerivedStateFromProps()(nextProps) {
if(nextProps.visible && ...) {
fetch(...).then(res => {
return {
product: res.data
}
}).catch(err => {
...
})
} else return null (or just return null here without else)
}
The above code doesn't work since fetch is asynchronous so it always returns null or doesn't return anything, you can't use await here to wait for the api to resolve also, and I heard that getDerivedStateFromProps shouldn't use for data fetching.
So what is the best way solve the problem ?
I think it's better to decide whether to show Modal component or not in parent component as Modal component should be a functional component to render only modal related view. This way every time Modal component will not be rendered and only rendered when visible flag is true.
{ this.state.visible &&
<Modal />
}
In parent component you could fetch data in componentDidMount if just after initial render the data is required or componentDidUpdate if after every update the fetch data is required for modal. After fetching data set state of visible to true.
Happy Coding!!!
You can mount the Modal based on this.state.visible and start fetching when Modal is mounted on componentDidMount or when the props are changing through componentDidUpdate
// delay
const delay = () => new Promise(res => setTimeout(res, 1000));
// fake products
const products = [
{ id: 1, text: "product 1" },
{ id: 2, text: "product 2" },
{ id: 3, text: "product 3" }
];
// fake ajax call
const API = async productId => {
await delay();
return products.find(p => p.id === productId);
};
class Parent extends Component {
state = { productId: 1, visible: false };
toggleShow = () => {
this.setState(prevState => ({ visible: !prevState.visible }));
};
setProductId = productId => this.setState({ productId, visible: true });
render() {
return (
<div className="App">
<button onClick={this.toggleShow}>show or hide</button>
<button onClick={() => this.setProductId(1)}>fetch product 1</button>
<button onClick={() => this.setProductId(2)}>fetch product 2</button>
<button onClick={() => this.setProductId(3)}>fetch product 3</button>
<button onClick={() => this.setProductId(4)}>unknown product id</button>
{this.state.visible && <Modal is={this.state.productId} />}
</div>
);
}
}
class Modal extends Component {
state = { product: null, fetching: false };
componentDidMount = () => {
this.fetchProduct();
};
componentDidUpdate = prevProps => {
if (prevProps.is !== this.props.is) {
this.fetchProduct();
}
};
fetchProduct = async () => {
this.setState({ fetching: true });
const product = await API(this.props.is);
this.setState({ product, fetching: false });
};
render() {
const { product, fetching } = this.state;
if (fetching) return <h1>{`fetching product ${this.props.is}`}</h1>;
return product ? (
<div>
<h1>{`product id: ${product.id}`}</h1>
<h3>{`text: ${product.text}`}</h3>
</div>
) : (
<h1>Product not found</h1>
);
}
}
SandBox
Actually in another approach, you can use <modal/> in another pureComponent
(for example: componentContainer) and just call it in your main view. and use just one object inside your main view state as your <componentContainer/> data scope and give it as a property like this code
construcor(prop){
super(props);
this.state={
productDetail:null<<change this in data fetch
}
}
<componentContainer data={this.state.productDetail} modalVisible={this.state.changeNumderModal}/>
and inside your component container:
<Modal
animationType="fade"
transparent={true}
visible={this.props.modalVisible}//<<<<<<<<<
onRequestClose={() => {
}}>
//contents:
this.props.data.map(()=>//-----)
</View>
</Modal>
in this method you have just one modal and one scope as its data, you can call the fetchs and other functions in your main component as others... and if its so needed you can even pass a function as property to modal container to :
someFunction(someval){
//do some thing
}
<componentContainer data={this.state.productDetail} modalVisible={this.state.changeNumderModal} someFunction={(val)=>this.someFunction(val)}/>
and call it inside :
this.props.someFunction('what ever you want')
So Let's assume you did that hide and show of modal with button click, now whenever the model will open
inside the componentWillMount function
class Modal extends Component {
this.state = {product:[], loader : false}
componentWillMount(){
fetch(...).then(res => {
this.setState({
product: res.data
})
}).catch(err => {
...
})
}
render(){
const {product, loader} = this.state;
return (
if (loader) return <ProductComponent data = {product}/>
else return <div> No data found </div>
);
}
}
You can use the approach below:
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.props.visible && !prevProps.visible) {
// make the API call here
}
}

How render components after fetching data

I have a container called "Recetas" (Receipes) which has inside a component called "Showcase". The idea is that the user can create a request in "Recetas" and when the data is fetched, the container will re-render and "Showcase" will update with the new data.
I'm saving the request in the state of "Recetas" and passing the data to "Showcase" with props.
The problem is that the render is being execute before I receive the new data. So I'm always showing "old" data. Is there any way I can put on hold the render until I've received the data? Or how can I solve it?
class Recetas extends Component {
state = {
loading: false,
data: [],
maxResult: 12,
minResult: 0,
query: 'burger',
appId: 'xxxxxx',
appKey: 'xxxxx'
}
componentDidMount() {
this.fetchData();
}
async fetchData() {
this.setState({loading: true});
console.log('fetching ...');
try {
const request = `https://api.edamam.com/search?q=${this.state.query}&app_id=${this.state.appId}&app_key=${this.state.appKey}&from=${this.state.minResult}&to=${this.state.maxResult}`;
console.log('request: ', request);
const result = await axios(request);
this.setState({ data: result.data.hits, loading: false });
} catch (error) {
console.log(error);
}
}
queryHandler = value => {
this.setState({
query: value
});
this.fetchData();
}
render() {
return (
<div className={classes.Recetas}>
{console.log('actualState: ', this.state)}
<SearchInput
query={this.state.query}
queryHandler={(value) => this.queryHandler(value)}/>
<Showcase
data={this.state.data}
loading={this.state.loading}/>
</div>
);
}
Showcase component
const showcase = props => {
const spinner = <Spinner />;
const recetas = props.data.map((elem, index) => {
return <Receta key={index} title={elem.recipe.label} url={elem.recipe.image} />
});
console.log('[Showcase] props.data: ', props.data);
return (
<div className={classes.Showcase}>
{props.loading ? spinner : recetas}
</div>
);
}
In case the Function Component not watch the props change. You should move the spinner to the container component.
Recetas.js
render() {
...
const {loading, data} = this.state
return (
...
<div className={classes.Recetas}>
{loading ? < Spinner /> : <Showcase data={data} />}
</div>
);
}
//////////////
ShowCase.js
const showcase = props => (
<div className={classes.Showcase}>
{
props.data.map(({recipe: {label, imgage}}, index) => <Receta key={index} title={label} url={image} />)
}
</div>
)
And using Destructuring_assignment for shorter code.
Finally I managed to solve it adding a setTimeout function which execute the fetch 500 ms after updating the state in 'queryhandler' method.

Setting State Array and Appending Value on Update

I'm still learning about state and lifecycle with ReactJS and have run into a scenario where I have a form that on submit should save the form value and then append the returned JSON object to the end of an array which would re-render the component storing the original array.
With my current setup, I have the components setup and form submit with returned JSON object, but the state contains an empty array rather than the object spread {...comment} and it doesn't look like the setState is updating component, but that could be due to the empty array mentioned before. Can anyone point me in the right direction?
Comment:
import React from 'react';
import fetch from 'node-fetch';
//record Comment - Comment Form Handle POST
class CommentForm extends React.Component {
constructor(props){
super(props);
this.state = {
value: '',
comments: []
};
this.onChange = this.onChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
postComment(comment, recordId, csrfToken) {
var body = { comment: comment };
var route = 'http://localhost:3000/record/' + recordId + '/comment';
fetch(route,
{
method: 'POST',
body: JSON.stringify(body),
headers: {
'X-CSRF-Token': csrfToken,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(res => {
return res.json();
})
.then(data => {
console.log(data);
let commentsArr = this.state.comments;
this.setState({comments: commentsArr.concat(data)});
})
.catch(err => {
console.log(err);
});
}
onChange(e){
this.setState({
value: e.target.value
});
}
handleSubmit(e){
e.preventDefault();
this.postComment(this.state.value, this.props.recordId, this.props.csrf);
}
render(){
return (
<div className="record-comment__form">
<div className="row">
<form action={"/record/" + this.props.recordId + "/comment"} method="post" onSubmit={this.handleSubmit}>
<input type="hidden" name="_csrf" value={this.props.csrf}/>
<textarea name="comment" className="record-comment__form-text-area" onChange={e => this.setState({ value: e.target.value })} value={this.state.value}></textarea>
<button type="submit" className="record-comment__form-button" disabled={!this.state.value}>Comment</button>
</form>
</div>
</div>
)
}
}
//record Comment - Comment
const Comment = props => {
return (
<div className="row">
<div className="col-md-12">
<h5>{props.user_id}</h5>
<h4>{props.comment}</h4>
<h3>{props.synotate_user.fullNameSlug}</h3>
</div>
</div>
)
}
//record Comment - Container
export default class Comments extends React.Component {
render() {
return (
<div className="record-comment-container">
<CommentForm recordId={this.props.recordId} csrf={this.props.csrf}/>
{ this.props.record_comments.map((comment, i) =>
<Comment {...comment} key={this.props.recordCommentId}/>
)}
</div>
);
}
}
Record (Parent component)(Where Comment is being set):
//GET /api/test and set to state
class RecordFeedContainer extends React.Component{
constructor(props, context) {
super(props, context);
this.state = this.context.data || window.__INITIAL_STATE__ || {records: []};
}
fetchList() {
fetch('http://localhost:3000/api/test')
.then(res => {
return res.json();
})
.then(data => {
console.log(data);
this.setState({ records: data.record, user: data.user, csrf: data.csrfToken });
})
.catch(err => {
console.log(err);
});
}
componentDidMount() {
this.fetchList();
}
render() {
return (
<div className="container">
<h2>Comments List</h2>
<RecordFeed {...this.state} />
</div>
)
}
};
//Loop through JSON and create Record and Comment Container Component
const RecordFeed = props => {
return (
<div>
{
props.records.map((record, index) => {
return (
<div className="row">
<div className="col-md-6 col-md-offset-3 record-card">
<RecordCard {...record} key={record.recordIdHash} user={props.user} />
<Comments {...record} key={index} recordId={record.recordIdHash} csrf={props.csrf}/>
</div>
</div>
);
})
}
</div>
)
}
Your problem is that when rendering <Comments>, the this.props.record_comments is not the comments you've updated in the state of the <CommentForm> component. Each component has it's own internal state.
You need to pass the state along to your <Comments> component. You will need to move your state up to the top level or use a state management system like Redux which will allow you to access a shared state which could contain your comments array.
From the top level component you could manage the state there, like so:
this.state = {
comments: [],
// other shared state
};
You can pass along an update comments function, named for example updateCommentsFunc() to <CommentForm> like so:
<CommentForm updateComments={this.updateCommentsFunc} recordId={this.props.recordId} csrf={this.props.csrf}/>
Which will allow you to pass the updated comments back up to the parent component via something like:
const updateCommentsFunc = (newComments) => {
this.setState({comments: [...this.state.comments, newComments]});
}
Your postComment() function doesn't appear to be properly bound to your enveloping <CommentForm/> component's this. As a result; calling this.setState() from within the function isn't really doing anything.
Try binding it within your constructor method.
constructor(props) {
// ...
this.postComment = this.postComment.bind(this)
}
Or by declaring it using an arrow function.
postComment = (comment, recordId, csrfToken) => {
// ...
}
See this article for more info on React binding patterns.

Resources