Fetch data and then render it to dom React - javascript

Hi I am fetching data from an api and I would like to take the data and render it to the dom but I am the error "Uncaught TypeError: Cannot read property 'map' of undefined at Topicselect.render"
Here is essentially what I am doing, although I have abstracted away anything that is not directly relevant to the question, such as actual topic names, imports, etc :
class Topics extends Component{
constructor(props){
super(props);
this.state = {
topics: []
}
}
componentWillMount(){
fetch('/api').then((res)=>r.json().then((data)=>{
// push topics into this.state.topics somehow
})
console.log(this.state.topics) //returns ['topic1','topic2','topic3'];
}
render(){
const list = this.state.topics.map((topic)=>{
return(<li>{topic}</li>);
})
return(
<ul>
{list}
</ul>
)
}
}
Can anyone tell me how to fix this? I saw an answer on here that said to use componentDidMount instead of componentWillMount but that isn't working for me

You are missing a closing bracket ) after the fetch and it's indeed recommended to use componentDidMount() instead of componentWillMount() for fetching data from an API.
Also don't forget to use this.setState({ topics: data.howeverYourDataIsStructured }); after you receive the data from the API to ensure a rerender of the component.
class Topics extends Component{
constructor(props){
super(props);
this.state = {
topics: []
}
}
componentDidMount() {
fetch('/api').then((res)=>r.json().then((data)=>{
this.setState({ topics: data.topics });
}));
console.log(this.state.topics) //returns [];
}
render() {
console.log(this.state.topics) //returns [] the first render, returns ['topic1','topic2','topic3'] on the second render;
return(
<ul>
{this.state.topics.map(topic => (
<li>{topic}</li>
))}
</ul>
)
}
}

Make sure you use setState() to update your state, otherwise render() won't be triggered to update the dom. Also make sure you don't just overwrite the current state but add your new topics to the old ones. (not relevant for this case, but still important to mention)
One way to do it would be:
componentDidMount() {
var currentTopics = this.state.topics;
fetch('/api').then((res) => r.json().then((data) => {
currentTopics.push(data);
}));
this.setState({'topics': currentTopics});
}
But you can also call setState() inside the loop. setState() does not work synchronously so it will first wait if there are some other changes to be made before it will actually execute the changes and then trigger render.
componentDidMount() {
fetch('/api').then((res) => r.json().then((data) => {
this.setState((state) => ({ topics: [...state.topics, data]}));
}));
}

Related

React: Can't access array data in component state with indices

As a practice project, I've started building a small Pokedex app in React.
import React, { Component} from 'react';
import './App.css';
import Card from './components/card/Card.component';
class App extends Component{
constructor(){
super();
this.state = {}
}
componentDidMount(){
let pokeDataArr = []
const getPokemonData = async() => {
const dataResponse = await fetch(
'https://pokeapi.co/api/v2/pokemon?limit=10'
);
const dataArr = await dataResponse.json();
const dataArr2 = await dataArr.results.forEach(i => {
fetch(i.url)
.then(dataResponse => dataResponse.json())
.then(json => pokeDataArr.push(json))
})
this.setState({ pokeDataArr }, () => console.log(this.state))
}
getPokemonData();
}
render(){
return(
<div>Pokedex!</div>
)
}
}
I'm having trouble accessing data from a specific index in an array.
When I log the entire state object to the console, I can see all the data I have retrieved from the AJAX call.
this.setState({ pokeDataArr }, () => console.log(this.state))
And this is the result in the console:
console result
However, if I try to log out data from an index in the array with:
this.setState({ pokeDataArr }, () => console.log(this.state.pokeDataArr[0]))
I get "undefined" in the console:
console result 2
As far as I'm aware, whatever function you run in the this.setState method's callback, it should run after setState has finished.
My goal is to use the data from this.state.pokeDataArr to make cards that display the info of each individual pokemon, but it seems like I'm stuck until I find a way to extract the data from the array and I have no clue what I'm missing.
Thank you for your time.
I think you messed up with your react state.
Usually, what people do is they set up their react state as an object with other elements (arrays, objects, strings, whatever) inside it. This looks something like this:
constructor(){
super();
this.state = {
myObject: {},
somethingElse: "",
anArray: []
}
}
This enables you to access parts of your state like this: this.state.myObject for instance. (this would return {})
In your example, you defined your state as an empty object.
constructor(){
super();
this.state = {}
}
And later, you set this object to an object, with an array inside:
this.setState({ pokeDataArr });
This will set your state to this: {[(your array)]}
To prevent this initialize your state like this:
constructor(){
super();
this.state = { pokeDataArr : {} }
}
And set your values like this:
this.setState({ pokeDataArr: pokeDataArr }, () => console.log(this.state.pokeDataArr[0]))
read more here: https://reactjs.org/docs/state-and-lifecycle.html
You'll need to use updater to use the callback instead of plain state update:
this.setState(
() => ({ pokeDataArr }),
() => console.log(this.state.pokeDataArr[0])
)
Read the note from the docs in the linked example:
Subsequent calls will override values from previous calls in the same cycle, so the quantity will only be incremented once. If the next state depends on the current state, we recommend using the updater function form

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],
)}

Cannot read property 'enqueueSetState' of undefined in ReactJS

I'm not quite sure why I'm getting this error.
class ContentProcessing extends Component {
constructor(props) {
super(props);
this.state = {content: currentData};
this.setData = this.setData.bind(this);
}
setData(data) {
this.setState({
content: data
});
}
render() {
return (
<div>
<Card title={this.state.content} />
</div>
);
}
}
The error is reported at
this.setState({
content: data
});
Basically I'm launching setData from a Button in another class, as soon as I click it my page breaks and I receive the error.
I checked and it looks like in setData(), this.state is undefined so I suppose that's probably where the problem comes from.
I've looked at a few other answers that were having this same problem but their fixes don't seem to be working for me.
This error is because this.setState in not bind to this in main class. If you want to pass setState to somewhere else you need to bind it first in its main class:
class ContentProcessing extends Component {
constructor(props) {
super(props);
this.state = {content: currentData};
this.setData = this.setData.bind(this);
this.setState = this.setState.bind(this); // <- try by adding this line
}}
Inside your constructor you have:
this.state = {content: currentData};
Where does currentData come from? If it's supposed to be passed as a prop, then change that line to:
this.state = {content: prop.currentData};
I guess you are calling setData from Card component.
If this is the case, send setData as a prop to Card component
// Below code snippet in ContentProcessing component
<div>
<Card
title={this.state.content}
setData={this.setData}
/>
</div>
Now you can access setData method in Card component as prop.
// Call it in Card component
this.props.setData(data);
This setData is useless as it will just stay undefined all the time
setData(data) {
this.setState({
content: data
});
}
You can some sort of event which will be responsible for setState() Something like :
render() {
return (
<div>
<Card title={this.state.content} onClick={() => {this.setData}}/>
</div>
);
}
}

Rendering react component after api response

I have a react component that I wish to populate with images using the Dropbox api. The api part works fine, but the component is rendered before the data comes through & so the array is empty. How can I delay the rendering of the component until it has the data it needs?
var fileList = [];
var images = [];
var imageSource = [];
class Foo extends React.Component {
render(){
dbx.filesListFolder({path: ''})
.then(function(response) {
fileList=response.entries;
for(var i=0; i<fileList.length; i++){
imageSource.push(fileList[0].path_lower);
}
console.log(imageSource);
})
for(var a=0; a<imageSource.length; a++){
images.push(<img key={a} className='images'/>);
}
return (
<div className="folioWrapper">
{images}
</div>
);
}
}
Thanks for your help!
Changes:
1. Don't do the api call inside render method, use componentDidMount lifecycle method for that.
componentDidMount:
componentDidMount() is invoked immediately after a component is
mounted. Initialization that requires DOM nodes should go here. If you
need to load data from a remote endpoint, this is a good place to
instantiate the network request. Setting state in this method will
trigger a re-rendering.
2. Define the imageSource variable in state array with initial value [], once you get the response update that using setState, it will automatically re-render the component with updated data.
3. Use the state array to generate the ui components in render method.
4. To hold the rendering until you didn't get the data, put the condition inside render method check the length of imageSource array if length is zero then return null.
Write it like this:
class Foo extends React.Component {
constructor(){
super();
this.state = {
imageSource: []
}
}
componentDidMount(){
dbx.filesListFolder({path: ''})
.then((response) => {
let fileList = response.entries;
this.setState({
imageSource: fileList
});
})
}
render(){
if(!this.state.imageSource.length)
return null;
let images = this.state.imageSource.map((el, i) => (
<img key={i} className='images' src={el.path_lower} />
))
return (
<div className="folioWrapper">
{images}
</div>
);
}
}
You should be using your component's state or props so that it will re-render when data is updated. The call to Dropbox should be done outside of the render method or else you'll be hitting the API every time the component re-renders. Here's an example of what you could do.
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
imageSource: []
}
}
componentDidMount() {
dbx.filesListFolder({ path: '' }).then(function(response) {
const fileList = response.entries;
this.setState({
imageSource: fileList.map(file => file.path_lower);
})
});
}
render() {
return (
<div className="folioWrapper">
{this.state.imageSource.map((image, i) => <img key={i} className="images" src={image} />)}
</div>
);
}
}
If there are no images yet, it'll just render an empty div this way.
First off, you should be using the component's state and not using globally defined variables.
So to avoid showing the component with an empty array of images, you'll need to apply a conditional "loading" class on the component and remove it when the array is no longer empty.

React API call failure

I'm new to React and I'm trying to build (for now) a simple app that allows you to search for a name in a list of transactions and return the transaction details to you. So far I have been struggling to deal with the API request and I keep running into errors. I'm using superagent for my API calls
import React, {Component } from 'react';
import Request from 'superagent'
import './App.css';
class App extends Component {
constructor() {
super();
this.state = {};
}
componentWillMount() {
var url = 'https://api.myjson.com/bins/2dorw';
Request.get(url).then((response) => {
this.setState({
items: response.body.items,
});
});
}
render() {
var names = this.state.items.map((name, index) => {
<div>
<li key={index}> {this.state.items.merchant.name } </li>
</div>
});
return (
<div className="App">
{names}
</div>
);
}
}
export default App;
I can't even make this simple API call to work, just display the names from the API endpoint. This has been going on for a day or so, and usually I get the object either null, undefined and type errors.
Uncaught (in promise) TypeError: Cannot read property '_currentElement' of null(…)
App.js:64 Uncaught TypeError: Cannot read property 'map' of undefined
I have tried following tutorials but I haven't been able to find one that would work in my case, so I can't wrap my head around the API call.
UPDATE
After changing to componentDidMount(), adding isFetching and setting items to an array in setState (as the two answers have suggested), I am now getting an
Uncaught (in promise) TypeError: Cannot read property 'name' of undefined
Using componentWillMount() is slightly early in the lifecycle. Wait until it has mounted using componentDidMount() That should help resolve the first error being thrown.
The second error comes from this.state.items.map when the items have not shown up yet. I would also suggest adding a isFetching state, to check if the data has shown up yet.:
class App extends Component {
constructor() {
super();
this.state = {
isFetching: true
};
}
componentDidMount() {
var url = 'https://api.myjson.com/bins/2dorw';
Request.get(url).then((response) => {
this.setState({
items: response.body.items,
isFetching: false
});
});
}
render() {
if (this.state.isFetching) return null;
var names = this.state.items.map((item, index) => {
<div>
<li key={index}> {item.merchant.name} </li>
</div >
});
return (
<div className="App">
{names}
</div>
);
}
}
export default App;
Update
Added a slight change - looks like your map function was also a bit off. It's hard to say what exactly you are looking for, because I don't know the data structure exactly.
You had
var names = this.state.items.map((name, index) => {
<div>
<li key={index}> {this.state.items.merchant.name } </li>
</div>
});
Passing name as the first param, which is the entire item. You then call this.state all over again - which you shouldn't need to do. I think this is where some of the undefined issues are coming from as well.
this.state.items is undefined because you declared state = {}, so can not map over it. You can fix this problem by making items an empty array.
constructor() {
super();
this.state = {
items: []
};
}

Categories

Resources