I am trying to pass data received in one component of a React application. On success I am taking the received data and setting the state and then trying to pass that data as a property of the next component. Once inside the second component I need to access the passed data via this.state so I can change the state in that component later. I seem to be encountering an issue with the DOM rendering before the data is received from the service. I have tried passing an already loaded array of values in place of this.state.data in <List data={this.state.data}/> and it seems to execute fine. How can assure that I have received the data from the service before rendering the DOM so that the data is passed all the way down to each component.
EDIT: added full implementation of the List element so explain the use of this.state
This is basically what I am trying to do:
var Box = React.createClass({
getInitialState: function() {
return {data: []};
},
loadTodosFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'GET',
cache: false,
success: function(dataResponse) {
this.setState({data: dataResponse});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
componentDidMount: function() {
this.loadFromServer();
},
render: function() {
return (<List data={this.state.data}/>);
}
});
var List = React.createClass({
getInitialState: function() {
return {data: this.props.data};
},
dragStart: function(e) {
this.dragged = e.currentTarget;
e.dataTransfer.effectAllowed = 'move';
// Firefox requires dataTransfer data to be set
e.dataTransfer.setData("text/html", e.currentTarget);
},
dragEnd: function(e) {
this.dragged.style.display = "block";
this.dragged.parentNode.removeChild(placeholder);
// Update data
var data = this.state.data;
var from = Number(this.dragged.dataset.id);
var to = Number(this.over.dataset.id);
if(from < to) to--;
if(this.nodePlacement == "after") to++;
data.splice(to, 0, data.splice(from, 1)[0]);
this.setState({data: data});
},
dragOver: function(e) {
e.preventDefault();
this.dragged.style.display = "none";
if(e.target.className == "placeholder") return;
this.over = e.target;
// Inside the dragOver method
var relY = e.clientY - this.over.offsetTop;
var height = this.over.offsetHeight / 2;
var parent = e.target.parentNode;
if(relY > height) {
this.nodePlacement = "after";
parent.insertBefore(placeholder, e.target.nextElementSibling);
}
else if(relY < height) {
this.nodePlacement = "before"
parent.insertBefore(placeholder, e.target);
}
},
render: function() {
var results = this.state.data;
return (
<ul>
{
results.map(function(result, i) {
return (
<li key={i}>{result}</li>
)
})
}
</ul>
);
}
});
ReactDOM.render(
<Box url="/api/comments"/>, document.getElementById('content')
);
The reason why your data load subsequent to component load is not rendering the data is because of this line in your List.render function:
var results = this.state.data;
Essentially, you have made a copy of your original props and assigned them to the state in the List component using the getInitialState method. And after that your state and props are delinked. Which means that if the props.data changes on the List component, the state doesn't know about it, so therefore nothing gets re-rendered.
So, instead of using state to initialize the results variable, use props.
var results = this.props.data
Here's how it would look like:
var List = React.createClass({
render: function() {
var results = this.props.data;
return (
<ul>
{
results.map(function(result, i) {
return (
<li key={i}>{result}</li>
)
})
}
</ul>
);
}
});
Now anytime the data changes, props get updated and eventually the results get re-rendered.
Updated to address the comments from the OP:
If you want to update the state of the list but want to be notified every time the props at the parent change, then you want to use the method componentWillReceiveProps so that when the data is obtained the child List is notified. And in this method you can set the new state:
componentWillReceiveProps: function(newProps) {
this.setState({data: this.props.data});
}
Once you do this, react will re-render the list for you.
Another update: To illustrate how this works I have put together an example here.
And here's the JS code for this:
let todos = ["Run","Swim","Skate"];
class MyList extends React.Component{
componentWillMount() {
console.log("Props are: ", this.props);
this.setState({list: this.props.items});
}
componentWillReceiveProps(newProps) {
console.log("Received Props are: ", newProps);
this.setState({list: newProps.items});
}
render() {
return (<ul>
{this.state.list.map((todo) => <li>{todo}</li>)}
</ul>);
}
}
class App extends React.Component{
constructor() {
super();
console.log("State is: ", this.state);
}
componentWillMount() {
this.setState({items: ["Fly"]});
}
componentDidMount() {
setTimeout(function(){
console.log("After 2 secs");
this.setState({items: todos});
}.bind(this), 2000);
}
render() {
return (<MyList items={this.state.items}/>);
}
}
ReactDOM.render(<App/>, document.getElementById("app"));
Related
In console.log the api fetched data are displaying but in browser itis
showing only white screen. In map function have to update the state function
import React, { Component } from 'react';;
import * as algoliasearch from "algoliasearch";
class App extends React.Component {
constructor() {
super();
this.state = {
data: { hits: [] }
}
// set data to string instead of an array
}
componentDidMount() {
this.getData();
}
getData() {
var client = algoliasearch('api-id', 'apikey');
var index = client.initIndex('');
//index.search({ query:""}, function(data){ console.log(data) })
//index.search({ query:""}, function(data){ console.log("DataRecib=ved. First check this") })
index.search({
query: "",
attributesToRetrieve: ['ItemRate', 'Color'],
hitsPerPage: 50,
},
function searchDone(error, data) {
console.log(data.hits)
});
}
render() {
return (
<div id="root">
{
this.state.data.hits.map(function (data, index) {
return
<h1>{this.setState.data.ItemRate}<br />{data.Color}</h1> >
})}
</div>
);
}
}
//render(<App />, document.getElementById('app'));
export default App;
Couple of mistakes -:
You just need to use this.state.data.ItemRate instead of this.setState.data.ItemRate.
You can get state inside .map using arrow functions ( . )=> { . }
Visit https://www.sitepoint.com/es6-arrow-functions-new-fat-concise-syntax-javascript/
render() {
return (
<div id="root" >
{
this.state.data.hits.map((data,index) => {
return<h1>{this.state.data.ItemRate}<br />{data.Color}</h1>
}
Every this.setState triggers a render() call. If you setState inside render method, you go into an infinity loop.
You want to update this.state.data.hits inside getData() function, then you can display the data like so:
this.state.data.hits.map(data =>
<h1>{data.Color}</h1>
)
For example, if console.log(data.hits) logs out the correct data, then you can:
this.setState({
data: {
hits: data.hits
}
})
EDIT:
Using the code you provided, it should be like this:'
getData = () => {
var client = algoliasearch('A5WV4Z1P6I', '9bc843cb2d00100efcf398f4890e1905');
var index = client.initIndex('dev_twinning');
//index.search({ query:""}, function(data){ console.log(data) })
// index.search({ query:""}, function(data){ console.log("Data Recib=ved. First check this") })
index.search({
query: "",
attributesToRetrieve: ['ItemRate', 'Color'],
hitsPerPage: 50,
}, searchDone = (error, data) => {
this.setState({
data: {
hits: data.hits
}
})
console.log(data.hits)
})
}
I have spent quite a time to read and try to make work for the data that I am trying to pass through.
The issue is that the state variable is an array type, when I do setState on same, componentWillReceiveProps does not have the right value for nextProps.
Following is the code snippet
This will render with Search component
var readOnlyTokens = ['ConnectionStatus = Down, Up'];
this.render = function () {
var self = this;
class SearchApp extends React.Component {
constructor(props) {
super(props);
this.state = {
value: [], // Issue with this variable
action: undefined
};
this.handleClick = this.handleClick.bind(this);
self.readonlySearchStatesAppInstance = this;
}
handleClick(e, value) {
console.log(value);
}
render() {
return (
<Search
id="readonly_modifyStates"
readOnly={true}
// value={readOnlyTokens.concat([])}
value={readOnlyTokens}
logicMenu={['OR']}
action={this.state.action}
/>
);
}
};
this.addReadOnlyTokens = function (tokens) {
// Value of tokens as ['Managed Status = InSync, OutSync']
this.readonlySearchStatesAppInstance.setState({value: tokens, action:"add"});
};
Search Component
class Search extends React.Component {
componentWillReceiveProps(nextProps) {
// Issue here is nextProps.value is still having the initial render value ['ConnectionStatus = Down, Up'] instead of ['Managed Status = InSync, OutSync']
switch(nextProps.action){
case "add":
this.searchWidget.addTokens(nextProps.value);
break;
}
}
render() {
return (
<div className="search-component"
ref={el => this.el = el}>
{this.props.children}
</div>
);
}
After reading at different answers, I have tried array.concat also - but for vain.
Any help is much appreciated..
Thanks
I'm trying to implement a restaurant app where a user can add dishes to a menu. The menu will be displayed in a side bar. Dish information is provided through an API. I'm having issues with the API requests/promises. I'm storing a list of the dishes in DinnerModel. I'm making the requests to the API in DinnerModel.
When I add a dish to the menu by clicking the add button in IngredientsList, I get redirected to a screen that shows Sidebar. But in Sidebar, the dishes are NaN. The console.logs show that this.state.menu in Sidebar is actually a Promise, not an array. I'm having trouble understanding why this is and what to do about it.
Note that update in Sidebar is supposed to run modelInstance.getFullMenu() which returns an array. But instead, a promise is returned. Why? What can I do to fix this?
Here's my code:
Dinnermodel.js:
const DinnerModel = function () {
let numberOfGuests = 4;
let observers = [];
let selectedDishes = [];
// API Calls
this.getAllDishes = function (query, type) {
const url = 'https://spoonacular-recipe-food-nutrition-v1.p.mashape.com/recipes/search?query='+query+"&type="+type;
return fetch(url, httpOptions)
.then(processResponse)
.catch(handleError)
}
//function that returns a dish of specific ID
this.getDish = function (id) {
let url = "https://spoonacular-recipe-food-nutrition-v1.p.mashape.com/recipes/"+id+"/information";
return fetch(url, httpOptions)
.then(processResponse)
.catch(handleError)
}
// API Helper methods
const processResponse = function (response) {
if (response.ok) {
return response.json();
}
throw response;
}
this.addToMenu = function(id, type){
var newDish = this.getDish(id).then()
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
}
//Returns all the dishes on the menu.
this.getFullMenu = function() {
return selectedDishes;
}
DishDetails.js:
class DishDetails extends Component {
constructor(props) {
super(props);
this.state = {
id: props.match.params.id,
status: "INITIAL",
type: props.match.params.type,
};
}
addToMenu (){
modelInstance.addToMenu(this.state.id, this.state.type);
this.props.history.push("/search/"+this.state.query+"/"+this.state.type);
}
componentDidMount = () => {
modelInstance.getDish(this.state.id)
.then(dish=> {
this.setState({
status:"LOADED",
ingredients: dish.extendedIngredients,
dishText: dish.winePairing.pairingText,
pricePerServing: dish.pricePerServing,
title: dish.title,
img: dish.image,
instructions: dish.instructions,
})
})
.catch(()=>{
this.setState({
status:"ERROR",
})
})
}
render() {
switch(this.state.status){
case "INITIAL":
return (
<p>Loading...</p>
);
case "ERROR":
return (
<p>An error has occurred, please refresh the page</p>
);
}
return (
<IngredientsList ingredients={this.state.ingredients} pricePerServing={this.state.pricePerServing} id={this.state.id} onButtonClick={() => this.addToMenu()}/>
<Sidebar />
);
}
}
export default withRouter(DishDetails);
Sidebar.js:
class Sidebar extends Component {
constructor(props) {
super(props)
// we put on state the properties we want to use and modify in the component
this.state = {
numberOfGuests: modelInstance.getNumberOfGuests(),
menu: modelInstance.getFullMenu(),
}
modelInstance.addObserver(this);
}
// this methods is called by React lifecycle when the
// component is actually shown to the user (mounted to DOM)
// that's a good place to setup model observer
componentDidMount() {
modelInstance.addObserver(this)
}
// this is called when component is removed from the DOM
// good place to remove observer
componentWillUnmount() {
modelInstance.removeObserver(this)
}
handleChangeGuests(event){
let noOfGuests = event.target.value;
modelInstance.setNumberOfGuests(noOfGuests);
}
// in our update function we modify the state which will
// cause the component to re-render
update() {
this.setState({
numberOfGuests: modelInstance.getNumberOfGuests(),
menu: modelInstance.getFullMenu(),
})
console.log("menu in Sidebar.js");
console.log(this.state.menu);
}
render() {
//console.log(this.state.menu);
let menu = this.state.menu.map((dish)=>
<div key={"menuitem-"+dish.id} className="menuitemwrapper">
<div className="menuitem">
<span className="dishname">{dish.title}</span>
<span className="dishprice">{dish.pricePerServing*modelInstance.getNumberOfGuests()}</span>
</div>
</div>
);
return (
<div id="sidebar-dishes">
{menu}
</div>
);
}
}
export default Sidebar;
IngredientsList.js:
class IngredientsList extends Component{
constructor(props){
super(props);
this.state = {
ingredients: props.ingredients,
pricePerServing: props.pricePerServing,
id: props.id,
noOfGuests: modelInstance.getNumberOfGuests(),
}
modelInstance.addObserver(this);
}
update(){
if(this._ismounted==true){
this.setState({
noOfGuests: modelInstance.getNumberOfGuests(),
});
}
}
componentDidMount(){
this._ismounted = true;
}
componentWillUnmount(){
this._ismounted = false;
}
render () {
return (
<button onClick={() => this.props.onButtonClick()} type="button" className="btn btn-default">Add to menu</button>
);
}
}
export default IngredientsList;
EDIT:
Changed DinneModel.addToMenu to:
this.addToMenu = function(id, type){
var newDish = this.getDish(id)
.then(()=>{
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
});
}
I still get a promise logged in the console from the console.log in Sidebar.js, and NaN in the Sidebar render.
getDish is not in your code posted, but I assume that it returns a promise. And this.getDish(id).then() also returns a promise. That’s why selectedDishes array has promises in it.
this.addToMenu = function(id, type){
var newDish = this.getDish(id).then()
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
}
To get actual newDish data, you need to use a callback function for the then.
this.addToMenu = function(id, type){
this.getDish(id).then(function (newDish) {
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
});
}
I have been searching for solutions for day, I learned a lot, but did not figure out what is wrong. Here what I do:
Calling the App constructor; initialising the state loading, dataSource and data
when the component mounts, the program calls the getData function with the requested URL
The getData function is an asynchronous fetch function
then the data is converted to JSON
then the JSON is cloned to became a datablob for the webview
then the setState function is called, changing the loading and the data.
This is where the setState does not fire. Not even the render (it should). Every tutorial, every forum shows it to be this way (and its also logical).
Here is the code:
import Exponent from 'exponent';
import React from 'react';
import {
StyleSheet,
Text,
View,
ListView,
ActivityIndicator
} from 'react-native';
import { Button, Card } from 'react-native-material-design';
import {UnitMenuCard} from './unitmenucard.js';
class App extends React.Component {
constructor(props) {
super(props);
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
loading: true,
dataSource: ds,
data: null
}
}
componentDidMount() {
//console.log('componentDidMount');
this.getData('https://dl.dropboxusercontent.com/u/76599014/testDATA.json');
}
getData(url) {
console.log('loading data');
return fetch(url).then(
(rawData) => {
console.log('parsing data');
//console.table(rawData);
return rawData.json();
}
).then(
(jsonData) =>
{
console.log('parsing to datablobs');
let datablobs = this.state.dataSource.cloneWithRows(jsonData);
console.log('datablobs: ' + datablobs);
return datablobs;
}
).then(
(datablobs) => {
console.log('setting state');
this.setState = ({
loading: false,
data: datablobs
});
console.log('the loading is ' + this.state.loading);
console.log('the data is ' + this.state.data);
}
).catch((errormsg) =>
{console.error('Loading error: ' + errormsg);}
);
}
render() {
console.log('loading is ' + this.state.loading);
var dataToDisplay = '';
if(this.state.loading) {
dataToDisplay = <ActivityIndicator animated={true} size='large' />
} else {
//let jdt = this.state.dataSource.cloneWithRows(this.state.data);
dataToDisplay = <ListView
dataSource={this.state.ds}
renderRow={(unit) => <UnitMenuCard name={unit.name} image={unit.picture} menu={unit.menu}/>}
/>
}
return (
<View style={styles.container}>
{dataToDisplay}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
});
Exponent.registerRootComponent(App);
Did I missed something? Thank you forward for your answer mighty Stack Overflow.
I think you misunderstood how DatSource works.
getInitialState: function() {
var ds = new ListViewDataSource({rowHasChanged: this._rowHasChanged});
return {ds};
},
_onDataArrived(newData) {
this._data = this._data.concat(newData);
this.setState({
ds: this.state.ds.cloneWithRows(this._data)
});
}
This is taken from the docs here: https://facebook.github.io/react-native/docs/listviewdatasource.html
See how you need to clone your datasource object in the state. The key thing is that you call cloneWithRows passing the data you got back from the API (in your case jsonData). This creates a cloned datasource, containing the data you just fetched.
In your code instead you just create a clone of your data source, but never replace the actual one you list view is linked to. You should do it this way:
.then(
(datablobs) => {
console.log('setting state');
this.setState = ({
loading: false,
dataSource: datablobs
});
console.log('the loading is ' + this.state.loading);
console.log('the data is ' + this.state.data);
}
You don't need state.data for this, the list view reads from the datasource object. You can also avoid having two .then calls by simply doing everything in one.
You also have another problem. You have the list view linked to the wrong property in the state. You list view code in render should be:
dataToDisplay = <ListView
dataSource={this.state.dataSource}
renderRow={(unit) => <UnitMenuCard name={unit.name} image={unit.picture} menu={unit.menu}/>}
/>
this.state.dataSource is where you store the data source object.
dataToDisplay = <ListView
dataSource={this.state.ds} // <- you set the dataSource to this.state.ds instead of this.state.data
renderRow={(unit) => <UnitMenuCard name={unit.name} image={unit.picture} menu={unit.menu}/>}
/>
The data source should be the data you got from your fetch call. Therefore, dataSource should be set equal to this.state.data.
My aim is to write a component that will be able to be used in others, providing a list of products, categories, and descriptions from an endpoint.
So far so good (console.log), BUT is looping in the componentDidMount the best way to go about this? Should I even loop it in GetData or do a forEach in another component that I want to use it in?
I'm relatively new to React so still trying to work out the best approach to this.
JS:
var GetData = React.createClass({
getInitialState: function() {
return {
categories: [],
productNames: [],
descriptions: []
};
},
componentDidMount: function() {
this.serverRequest = $.get("HTTPS://ENDPOINT", function (result) {
var products = result;
for (var i = 0; i < products.data.length; i++) {
this.setState({
categories : products.data[i].categories[0].title,
productNames : products.data[i].title,
descriptions : products.data[i].descriptions
});
}
}.bind(this));
},
componentWillUnmount: function() {
this.serverRequest.abort();
},
render: function() {
console.log(this.state.categories);
// console.log(this.state.productNames);
// console.log(this.state.descriptions);
return (
this.props.categories.forEach(function(category) {
// ??
},
this.props.products.forEach(function(product) {
// ??
},
this.props.descriptions.forEach(function(description) {
// ??
},
)
}
});
You can create a "smart" component that will be responsible only for making the Ajax Request without rendering anything in the UI apart from Categories Products and Descriptions. Another thing that you can do is to move your ajax request to componentWillMount. So in your case I would write something like that :
import Category from 'components/Category';
import Description from 'components/Description';
import Product from 'components/Product';
export default class AjaxRequestComponent extends Component
{
componentWillMount: function() {
$.get("HTTPS://ENDPOINT", function (result) {
var products = result;
for (var i = 0; i < products.data.length; i++) {
this.setState({
categories : products.data[i].categories[0].title,
productNames : products.data[i].title,
descriptions : products.data[i].descriptions
});
}
}.bind(this));
}
render(){
return(
{
this.state.descriptions.map( d => <Description {...d} />)
this.state.products.map( p => <Products {...p} />)
this.state.categories.map( c => <Category {...c} />)
}
)
}
}
Category, Products and Description are "dumb" components only responsible for presenting the data that you pass to them.
componentDidMount: function() {
this.serverRequest = $.get("HTTPS://ENDPOINT", function (result) {
var products = result;
var categories = products.data.map(function(item){
return item.categories[0].title;
};
var productNames = products.data.map(function(item){
return item.title;
};
var descriptions= products.data.map(function(item){
return item.descriptions;
};
}
this.setState({
categories : categories ,
productNames : productNames ,
descriptions : descriptions
});
}.bind(this));
},
in render function u can still use map function to return Array of React Component like example below
render: function() {
return (
<div>
{this.state.categories.map(function(category) {
return <span>{category}</span>
}}
</div>
)
}