Handling fetched objects in React components - javascript

I'm a React novice, learning on FCC, and I'm tearing my hair out trying to work with a fetched object.
I'm trying to build a basic 'Random Quote Machine' React component which fetches (and then renders values from) an object. The object (fetched in componentDidMount) has a single key (quotes) which is an array of objects (each of which has quote and author keys), and I assign this array to local state (quotesData) and then as a constant in the render.
Essentially, the component should display a random quote on first render and then again each time the New Quote button is clicked. So, I assign (at random) one of the objects in the array to nextQuote.
It's after this that I run into problems. I can console.log(quotesData) (and nextQuote) without issue, but why can I not then assign values from the object stored in nextQuote to nextQuoteQuote or nextQuoteAuthor, using dot (or bracket) notation?
Here's a link to the pen (the full code of which is also below): https://codepen.io/igorgetmeabrain/pen/rNmwMEM
Right now, it just keeps throwing object errors at me and won't render at all. Please help!
class RandomQuotes extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isFetching: false,
quotesData: [],
random: Math.random()
};
this.getNewQuote = this.getNewQuote.bind(this);
this.fetchData = this.fetchData.bind(this);
}
fetchData() {
this.setState({isFetching: true});
fetch ('https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json',
{headers: {
Accept: 'application/json'
}})
.then(res => res.json())
.then((data) =>
{
this.setState({quotesData: data.quotes, isFetching: false});
})
.catch(error => {
console.log(error);
this.setState({isFetching: false, error: error});
});
}
getNewQuote() {
this.setState({
random: Math.random()
});
}
componentDidMount() {
this.fetchData();
}
render() {
const {quotesData, isFetching, error} = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (isFetching) {
return <div>Loading...</div>;
} else {
const randomIndex = Math.floor(this.state.random * quotesData.length);
const nextQuote = quotesData[randomIndex];
const nextQuoteQuote = nextQuote.quote;
const nextQuoteAuthor = nextQuote.author;
return (
<div id="quote-box">
<p id="text">{nextQuoteQuote}</p>
<p id="author">{nextQuoteAuthor}</p>
<button id="new-quote" onClick={this.getNewQuote}>
New Quote
</button>
</div>
);
}
}
}
ReactDOM.render(<RandomQuotes />, document.getElementById("root"));

The fastest fix would be to change isFetching: true in the state.
It happens because in the beginning if isFetching: false and error: false it goes into last } else { part where you define
const nextQuoteQuote = nextQuote.quote;
const nextQuoteAuthor = nextQuote.author;
which are undefined because data is fetching in the background and they are not fetched yet.
So basically when you have to fetch data in the componentDidMount method you set loading state to true from the beginning so component knows it has to wait for it to fetch and later on show the data.

Related

Fetch one JSON object and use it reactjs

I'm very new to JS and ReactJS and I try to fetch an endpoint which gives me a JSON Object like this :
{"IDPRODUCT":4317892,"DESCRIPTION":"Some product of the store"}
I get this JSON Object by this endpoint :
http://localhost:3000/product/4317892
But I dont how to use it in my react application, I want to use those datas to display them on the page
My current code looks like this but it's not working and I'm sure not good too :
import React, {Component} from 'react';
class Products extends Component {
constructor(props){
super(props);
this.state = {
posts: {}
};
};
componentWillMount() {
fetch('http://localhost:3000/product/4317892')
.then(res => res.json())
.then(res => {
this.setState({
res
})
})
.catch((error => {
console.error(error);
}));
}
render() {
console.log(this.state)
const { postItems } = this.state;
return (
<div>
{postItems}
</div>
);
}
}
export default Products;
In the console.log(this.state) there is the data, but I'm so confused right now, dont know what to do
Since I'm here, I have one more question, I want to have an input in my App.js where the user will be able to type the product's id and get one, how can I manage to do that ? Passing the data from App.js to Products.js which is going to get the data and display them
Thank you all in advance
Your state doesn't have a postItems property which is considered undefined and react therefore would not render. In your situation there is no need to define a new const and use the state directly.
Also, when you setState(), you need to tell it which state property it should set the value to.
componentWillMount() {
fetch('http://localhost:3000/product/4317892')
.then(res => res.json())
.then(res => {
this.setState({
...this.state, // Not required but just a heads up on using mutation
posts: res
})
})
.catch((error => {
console.error(error);
}));
}
render() {
console.log(this.state)
return (
<div>
<p><strong>Id: {this.state.posts.IDPRODUCT}</strong></p>
<p>Description: {this.state.posts.DESCRIPTION}</p>
</div>
);
}
I have got 3 names for the same thing in your js: posts, postItems and res.
React can not determine for you that posts = postItems = res.
So make changes like this:
-
this.state = {
postItems: {}
};
-
this.setState({
postItems: res
});
-
return (
<div>
{JSON.stringify(postItems)}
<div>
<span>{postItems.IDPRODUCT}</span>
<span>{postItems.DESCRIPTION}</span>
</div>
</div>
);
{postItems["IDPRODUCT"]}
Will display the first value. You can do the same for the other value. Alternatively, you can put
{JSON.stringify(postItems)}
With respect to taking input in the App to use in this component, you can pass that input down through the props and access it in this component via this.props.myInput. In your app it'll look like this:
<Products myInput={someInput} />

set multiple states, and push to state of array in one onClick function

I'm running into a recurring issue in my code where I want to grab multiple pieces of data from a component to set as states, and push those into an array which is having its own state updated. The way I am doing it currently isn't working and I think it's because I do not understand the order of the way things happen in js and react.
Here's an example of something I'm doing that doesn't work: jsfiddle here or code below.
import React, {Component} from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
}
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
setCategoryStates = (categoryTitle, categorySubtitle) => {
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
render() {
return (
<CategoryComponent
setCategoryStates={this.setCategoryStates}
categoryTitle={'Category Title Text'}
categorySubtitle={'Category Subtitle Text'}
/>
);
}
}
class CategoryComponent extends Component {
render() {
var categoryTitle = this.props.categoryTitle;
var categorySubtitle = this.props.categorySubtitle;
return (
<div onClick={() => (this.props.setCategoryStates(
categoryTitle,
categorySubtitle,
))}
>
<h1>{categoryTitle}</h1>
<h2>{categorySubtitle}</h2>
</div>
);
}
}
I can see in the console that I am grabbing the categoryTitle and categorySubtitle that I want, but they get pushed as null into this.state.categoryArray. Is this a scenario where I need to be using promises? Taking another approach?
This occurs because setState is asynchronous (https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly).
Here's the problem
//State has categoryTitle as null and categorySubtitle as null.
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
//This gets the correct values in the parameters
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
//This method is using the state, which as can be seen from the constructor is null and hence you're pushing null into your array.
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
Solution to your problem: pass callback to setState
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
}, () => {
/*
Add state to the array
This callback will be called once the async state update has succeeded
So accessing state in this variable will be correct.
*/
this.pushToCategoryArray()
})
}
and change
pushToCategoryArray = () => {
//You don't need state, you can simply make these regular JavaScript variables
this.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
I think React doesn't re-render because of the pushToCategoryArray that directly change state. Need to assign new array in this.setState function.
// this.state.categoryArray.push({...})
const prevCategoryArray = this.state.categoryArray
this.setState({
categoryArray: [ newObject, ...prevCategoryArray],
)}

How do I create an array after Axios response and Display it in React js

I am having trouble creating an array of titles from an Axios response. The method getTitles(props) receives data from the Axios response. How do I create an array of titles dynamically?
The functions I have tried in Javascript are for loops and EC6 mapping, nothing seems to work. Being new to react I could be missing something but I am not sure what it is.
React code
export default class Featured extends React.Component {
constructor() {
super();
this.state = {
data: null,
}
}
/**
* Received request from server
*/
componentDidMount(){
ApiCalls.articleData()
.then(function(data){
this.setState(function(){
return {
data: data
}
})
}.bind(this));
}
getTitles(props){
//-- What code do I place here?
console.log(props.data)
return ['test title', 'test title 2'];
}
/**
* Render request
*/
render() {
let dataResponse = JSON.stringify(this.state.data, null, 2);
const Articles = this.getTitles(this.state).map((title, i) => <Article key={i} title={title}/> );
return (
<div class="row">{Articles}
<pre>{dataResponse}</pre>
</div>
);
}
}
Axios Code
var ApiCalls = {
articleData: function(id){
return axios.all([getArticles(id)])
.then(function(arr){
return arr[0].data.data;
})
.catch(function (error) {
console.log(error);
})
},
React setState behaves asynchronously . Your articles get rendered before the ajax was called and was not re rendered due to asynchrony in setState.
This is what doc(https://reactjs.org/docs/react-component.html#setstate) says
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
You can render the article after successful ajax call like below
componentDidMount(){
ApiCalls.articleData()
.then(function(data){
render(<Article data={data}/>, document.getElementById('...'));
}.bind(this));
}
Because of the post above, I was able to fix the issue by the code example below To see a working example goto the git repo
ApiCalls.articleData()
.then(function(data){
const newData = data.map(c => {
return c.attributes.title;
})
const addElement = newData.map((title, i) => <ContentTiles key={i} title={title}/> );
const newState = Object.assign({}, this.state, {
newData: addElement
});
this.setState(newState);
}.bind(this));

Connected component initial render fails because Redux state is not yet populated from server

I am using Redux thunk and axios to make server calls and modify the state depending on the result.
The problem is that when I use a connected component, and its initial state depends on data from the server, it does not render (the connected props are empty)
render () (<div>{this.props.someData}</data>) // empty, or error, if nested
...
const mapStateToProps = state => ({
someData: state.someData
})
I also tried this:
componentWillMount = () => {
this.setState({
someData: this.props.someData
})
}
And used state in render, but it didn't help.
Is there a way to wait for the server response before rendering or some other solution?
You can conditionally render that part. Use a property to indicate fetching status (property name is loading in my example).
class UserDetails extends React.Component {
state = {
loading: true,
data: null
}
fetch() {
this.setState({
loading: true
})
// an axios call in your case
setTimeout(() => this.setState({
loading: false,
data: {
nestedValue: 'nested value'
}
}), 500)
}
componentDidMount() {
this.fetch()
}
render() {
return <div>
{this.state.loading ? <span>loading...</span> : <div>nested prop value: {this.state.data.nestedValue}</div>}
</div>
}
}
Typically you would use the Component.defaultProps object to initialize your props. So in your case, something like this will set your someData prop to an initial value before axios receives the data.
class MyComponent extends React.Component {
// ... your implementation
}
MyComponent.defaultProps = {
someData: [] // empty array or whatever the data type is
};
Edit: docs

How store data from fetch

I'm pretty new in React and need some help.
I wanted display data from a movie database based on the search term. I'm using fetch inside my getMovies methode to get the data. The data is stored in data.Search but I don't know how to access it and store it in a variable.
class DataService
{
getMovies (searchTerm) {
//let dataStorage; //store somehow data.Search inside
fetch("http://www.omdbapi.com/?s=" + searchTerm, {
method: 'get'
})
.then((resp) => resp.json())
.then(function(data) {
return data.Search;
}).catch(function(error) {
console.log(error);// Error :(
});
}
//return dataStorage; //return data.Search
}
The below code is the correct react's way for your case, as simple as this:
import React from 'react';
export default class DataService extends React.Component {
constructor(props) {
super(props);
this.state = {
search_data: [], //for storing videos
};
this.getMovies = this.getMovies.bind(this); //bind this function to the current React Component
}
getMovies (searchTerm) {
fetch("http://www.omdbapi.com/?s=" + searchTerm, {
method: 'get'
})
.then((resp) => resp.json())
.then((data) => { //so that this callback function is bound to this React Component by itself
// Set state to bind search results into "search_data"
// or you can set "dataStorage = data.Search" here
// however, fetch is asynchronous, so using state is the correct way, it will update your view automatically if you render like I do below (in the render method)
this.setState({
search_data: data.Search,
});
});
}
componentDidMount() {
this.getMovies(); //start the fetch function here after elements appeared on page already
}
render() {
return (
{this.state.search_data.map((video, i) =>{
console.log(video);
// please use the correct key for your video below, I just assume it has a title
return(
<div>{video.title}</div>
)
})}
);
}
}
Feel free to post here any errors you may have, thanks
There are several ways of doing asynchronous tasks with React. The most basic one is to use setState and launch a Promise in the event handler. This might be viable for basic tasks but later on, you will encounter race conditions and other nasty stuff.
In order to do so, your service should return a Promise of results. On the React side when the query changes, the service is called to fetch new results. While doing so, there are few state transitions: setting loading flag in order to notify the user that the task is pending and when the promise resolves or rejects the data or an error is stored in the component. All you need is setState method.
More advanced techniques are based on Redux with redux-thunk or redux-saga middlewares. You may also consider RxJS - it is created especially for that kind of stuff providing debouncing, cancellation and other features out of the box.
Please see the following example of simple search view using yours DataService.
class DataService
{
//returns a Promise of search results, let the consumer handle errors
getMovies (searchTerm) {
return fetch("http://www.omdbapi.com/?s=" + searchTerm, {
method: 'get'
})
.then((resp) => resp.json())
.then(function(data) {
return data.Search;
})
}
}
const SearchResults = ({data, loading, error}) =>
<div>
{
data && data.map(({Title, Year, imdbID, Type, Poster}) =>
<div key={imdbID}>
{Title} - {Year} - {Type}
<img src={Poster} />
</div>
)
}
{loading && <div>Loading...</div>}
{error && <div>Error {error.message}</div>}
</div>
class SearchExample extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this)
this.state = {
data: [],
loading: false
};
}
handleChange(event) {
const service = new DataService();
//search query is target's value
const promise = service.getMovies(event.target.value);
//show that search is being performed
this.setState({
loading: true
})
//after the promise is resolved display the data or the error
promise.then(results => {
this.setState({
loading: false,
data: results
})
})
.catch(error => {
this.setState({
loading: false,
error: error
})
})
}
render() {
return (
<div>
<input placeholder="Search..." onChange={this.handleChange} type="search" />
<SearchResults data={this.state.data} loading={this.state.loading} error={this.state.error} />
</div>
)
}
}
ReactDOM.render(<SearchExample />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>

Categories

Resources