I know that there are many posts out there about how to use fetch and returning promises from async functions. I've read that you should move the bit of the code that relies on the fetched data/content into the function, but I can't see how that's possible with my setup. So before you close my question as a duplicate, I ask that you read it entirely and try to remember what it was like when you were new to programming and needed some help.
I'm building a React app in which I want to graph a bunch of CSV data depending on user input. I'm trying to fetch the CSV data, pull the two variables out that I want, and then pass it into a VictoryChart. This bit of code successfully fetches the data, converts it to an array separated by the record, and then creates a new array of objects (the data input VictoryChart requires) with only the two fields I want to be my variables:
async function getData(filePath) {
let dataArray = []
await fetch(filePath)
.then(response => {
return response.text()
})
.then(data => CSVToArray(data))
.then(data => {
for (let i in data) {
dataArray.push(
{ DateTime: data[i][0], OATemp: data[i][1]}
)
};
return dataArray
})
.catch(error => alert(error))
}
As suggested by several sources, I was going to try to put the VictoryChart into the function after the return dataArray to keep everything that relied on the function in the function, but I don't see how it's possible to control the layout of my page by trying to call a function that then generates a graph. I don't see how you build the DOM with that (am I even using that right?). I'm new to React (and generally an inexperienced programmer) so maybe that's part of the problem.
The only thing that makes sense to me is for the async function to return the data so I can pass it into the VictoryChart. But it seems impossible to extract anything returned from the function out of the promise that it gets wrapped into. I've been told to use .then but that just ends up returning a promise. It seems so incestual... To be specific my most recent attempt looks something like this (just using alert to "see" what's happening):
alert(getData('./2021WeatherData.csv').then(data => {return data}))
Hopefully, I've just not understood how to properly use .then as suggested by others. I have sunk many, many hours into trying dozens of different versions of this and googling every synonym of the problem I can think of. I know I'm missing something so I appreciate any help.
***Providing additional code
Below is what I understand to be the DOM for my React app (please correct me where I'm wrong in my wording etc.). This renders my app to the browser.
return (
<div className="App">
<header className="App-header">
<div className='Header-button-container'>
<button onClick={() => alert('Weather')}>
Weather
</button>
<button onClick={() => alert('1st Floor')}>
1st Floor
</button>
<button onClick={() => alert('2nd Floor')}>
2nd Floor
</button>
</div>
</header>
<VictoryChart>
<VictoryLine data={getData('./2021WeatherData.csv').then(data => { return data })} x='DateTime' y='OATemp' />
</VictoryChart>
</div>
);
To me, this dictates how things show up on the page. I notice that everything here is a tag so I don't understand how I would insert a function where the VictoryLine tag exists. The "data" attribute of the VictoryLine graph requires data as an array of objects and that's what I'm trying to output from my getData function (but it keeps getting wrapped in a darn promise!!). I am able to make it work just fine with a dummy data set I declare within the app, but I'm stuck when trying to fetch data. And since I can't get the data "out" of the function (it seems all that's possible is doing things inside of the function), I'm just stuck and perplexed. The intent here is to be able to click a button and fetch the relevant data to graph.
It sounds like you're just having trouble getting the CSV result passed along to your graphing function. First, you need to return your value from getData; currently it isn't returning anything:
async function getData(filePath) {
let dataArray = []
return fetch(filePath)
...
}
Then you can pass your result in one of two ways:
function theGraphFunction(data) {
// this graphs your data
}
getData("./path/to/data.csv").then(data => theGraphFunction(data));
or
(async () => {
const data = await getData("./path/to/data.csv");
theGraphFunction(data);
})()
Now that you've updated your question to include information about what you're actually trying to do, you could do something like this:
import {useEffect, useState} from "react";
function App() {
const [graphData, setGraphData] = useState(null);
useEffect(() => {
getData("./path/to/data.csv").then(data => setGraphData(data));
}, [])
// only include this if you don't want to render the component
// until data is loaded
if(!graphData) {
return null;
}
return (
<div className="App">
<header className="App-header">
<div className='Header-button-container'>
<button onClick={() => alert('Weather')}>
Weather
</button>
<button onClick={() => alert('1st Floor')}>
1st Floor
</button>
<button onClick={() => alert('2nd Floor')}>
2nd Floor
</button>
</div>
</header>
<VictoryChart>
<VictoryLine data={graphData} x='DateTime' y='OATemp' />
</VictoryChart>
</div>
);
}
Related
I'm slowly understanding the services and fetch with React but when I try to show something, it shows me absolutely nothing. I put the code in case someone can help me. Maybe I have to look at how to work with JSON, I don't know.
let datosApi = [];
const recogerDatos = () => {
let json = "https://jsonplaceholder.typicode.com/albums";
let miApi = "http://localhost/dsw/api/";
fetch(json)
.then(data => data.json())
.then(info => {
console.log(info);
this.datosApi = info;
})
}
function Services() {
return (
<>
{datosApi.map(datos => (
<p>{datos.title}</p>
))}
</>
);
}
export default Services;
JSON data appears in https://jsonplaceholder.typicode.com/albums
I think your example is missing something or you've not done it.
Basically there's a few things wrong:
recogerDatos is never being called
datosApi is not declared, and even if it was, it's not stateful, thus won't cause a re-render of your items.
I've created a working sandbox here that shows it working, and the code is below:
const [result, setResult] = useState([]);
const recogerDatos = () => {
let json = "https://jsonplaceholder.typicode.com/albums";
fetch(json)
.then((data) => data.json())
.then((info) => {
console.log(info);
setResult(info);
});
};
useEffect(() => {
recogerDatos();
}, []);
return (
<div className="App">
{result.length > 0 && result.map((datos) => <p>{datos.title}</p>)}
</div>
);
The recogerDatos function is called on page load (see useEffect), and the result is updated when the fetch is successful. This causes a re-render and the items are shown.
You are displaying data from your list
let datosApi = [];
However it does not seem like you are populating your list with data from the API since the method recogerDatos() is never being called.
From your code it seems like you're missing some core React patterns like state management and the components lifecycle. Since React re-renders components a lot you want to store things like fetched data into state to avoid them constantly reset to their initial value. And when you want to fetch the data you usually don't want to do it at each re-render (that would cause A LOT of fetching), instead you usually want to trigger it based on different events, for example when component (that will be used to show this data) is mounted. Such things are usually using the components lifecycle methods / useEffect hooks to ensure that they happen at the right point in time. I recommend you to go into React documentation and study the basics a bit more so you can understand the main concepts when you're coding, but here's a quick sandbox with an example of how you could get the desired effect in React:
https://codesandbox.io/s/beautiful-frost-on9wmn?file=/src/App.js
I wonder what a good way is to ignore earlier but delayed AJAX data return, when a newer AJAX call should be taken care of instead?
For example, if the first data fetch that started at 12:01:33 is delayed and returns at 12:01;39, and the second data fetch that started at 12:01:36 actually came back first, at 12:01:38. So the data returned at 12:01:39 should not be honored and should not be displayed onto the webpage and "cover up" the result of the second data fetch.
In some case, we might disable the Search or Submit button so that the user cannot click on the button again, but what about if there is a text input box, and the user typed "React" and press Enter, but he changed his mind and added "Redux" to the search box and pressed Enter again, and results returned by the "React" search should then be ignored (for both of the cases if this first AJAX returns either before or after the second AJAX), and only results returned by "React Redux" should be taken care of, to populate into the search result area down below.
In my past jobs, it seems that nobody actually took care of things like this. I can think of one way, which is:
let fetchTimeStampToHonor;
function getData() {
let currentFetchTimestamp = Date.now();
fetchTimeStampToHonor = currentFetchTimestamp;
fetch("...")
.then(res => res.json())
.then(data => {
if (currentFetchTimestamp === fetchTimeStampToHonor) { // honor it
}
});
}
so that if there is a newer call to getData(), then fetchTimeStampToHonor will get updated to the newest currentFetchTimestamp. The function passed to then is a closure and is able to access the local currentFetchTimestamp, and can be compared with fetchTimeStampToHonor. But the code above involves a global variable, which I can hide if I use a function to return getData(), and this way, I can hide fetchTimeStampToHonor inside of local scope and make it private. (but I think if this is inside of a module, it is private to the module and isn't such a big problem). Or it might be able to be made into some state if it is in ReactJS.
But are there some other better solutions?
You could produce a local cache of results. Use the search term as the key, then only show the relevant results. The code below is far from optimised and I would NOT use it as is, but I hope it illustrates the point.
I've used react in the example as it provides an easy way to show stateful changes but this concept could be done with raw JS as well.
const App = () => {
const [searchTerm, setSearchTerm] = useState('')
const [dataCache, setDataCache] = useState({})
useEffect(() => {
fetch(`search?term=${searchTerm}`)
.then((res) => res.json())
.then((data) => {
setDataCache((prev) => ({
...prev,
[searchTerm]: data,
}))
})
}, [searchTerm])
return (
<>
<input type="search" onChange={(e) => setSearchTerm(e.target.value)} />
<Results data={dataCache[searchTerm]} />
</>
)
}
Consider these two setState variables ,whose values change after a promise was successful (ex . ajax call)
const [ajax_response,getAjax_response] = useState("awaiting response"); //This one stores the ajax call response
const [showAjax,setShowAjax] = useState([]) //This one displays it
The promise is made using the useEffect hook which is called when specified (in this example in the first render)
useEffect(() => {
fetchData().then(data => {
//Here it stores the value in the first useState variable
getAjax_response(data[0])
//Here it loops throw the data and DYNAMICALLY CREATES some attributes based on the repsponse
//In this example it just fetches a single number from the ajax call
//And performs a loop until it reaches that (IRELEVANT)
for (let i=0;i < data[0];i++) {
let styled_data = <p onClick={() => console.log(ajax_response)}>{i}</p>;
setShowAjax(prev => [...prev,styled_data])
}
})
},[])
Basically what happens :
The value returned from the promise is set to ajax_response, let's say that it was CAT
the showAjax is set to a JSX element with a onClick={} Attribute
The problem :
If this was returned in our render
return (
<div className="App">
<button onClick={() => console.log(ajax_response)}>THE NORMAL BUTTON</button> //Button set manually
{showAjax} //The JSX elements set dynamically
</div>
);
the THE NORMAL BUTTON would console.log() the ajax_response and get the correct answer
BUT THE DYNAMICALLY CREATED BUTTONS IN {showAjax} console-logs only the INITIAL STATE of ajax_response
so the output looks like this
Generated inside {showAjax}
>>> awaiting response
Normal Button
>>> CAT
TL:DR
REACT ELEMENTS GENERATED DYNAMICALLY WITH ONCLICK ATTRIBUTES ARE BROKEN: THEY DO NOT SHOW UPDATED STATE
let styled_data = <p onClick={() => console.log(ajax_response)}>{i}</p>;
setShowAjax(prev => [...prev,styled_data])
The issue is that you are storing react elements in state. This is usually a bad idea precisely because of the issue you're run into here: they will never rerender, and thus it's really easy to create bugs where you render stale data. The function in this <p> closes over the value of ajax_response that existed when the useEffect ran. Ie, it has "awaiting response" in its closure, and always will.
Instead, your state should be the data that you need to create the components, and then you'll create the components when rendering.
for (let i=0;i < data[0];i++) {
// Modify this to store whatever data you need.
// Your example only used numbers, so i replicated that, but i expect your real code needs more.
setShowAjax(prev => [...prev, i]);
}
//...
return (
<div className="App">
<button onClick={() => console.log(ajax_response)}>THE NORMAL BUTTON</button>
{showAjax.map(value => (
<p onClick={() => console.log(ajax_response)}>{value}</p>
)}
</div>
);
This question already has answers here:
if-else statement inside jsx: ReactJS
(17 answers)
Closed 2 years ago.
I am new to React and as I was creating a project, I came across a rather peculiar event with my code.
It is with the async nature of the setState which doesn't let me render data on my page like i want it to.
I am trying to display files which i have in my database already, onto my webpage. But since the state is set after a while therefore i am not able to render my files onto the screen using the map function, since when it is called it is undefined.
I implplemented a get method in so as to get the json response of the files that i want to display. But as I mount my component and setstate of the Files, it shows that it doesn't have any value in it.I know it is async but i have no idea how to handle it so that i can use map to display onto the webpage.
My code is as follows:
import React, {Component} from "react";
// import axios from 'axios';
import {Link} from 'react-router-dom';
import {Styles} from '../Styling/Styles';
class FileView extends Component {
state = {
fileViewData : {}
}
// viewFunction(){
componentDidMount(){
fetch('http://127.0.0.1:8000/api/uploads/', {
method: 'GET'
})
.then((response) => {
let data = response.json();
return data;
})
.then((data) => {
this.setState({fileViewData: data}, ()=>{
console.log("hi");
});
}).catch(error => {console.log(error)})
// console.log(fileViewData);
}
render(){
return (
<React.Fragment>
<Styles>
<div className = "appbar">
<Link to='/dashboard'>
<button className="homeBtn" label="Back" >
Back
</button>
</Link>
</div>
{/* <button label="View" onClick={this.viewFunction} >
View
</button> */}
</Styles>
//.....section not working since map is not a function of undef......//
{
this.state.fileViewData.map(item =>{
return (
<h2>{item.fields.file}</h2>
);
})
}
//.......section not working ends.........//
{console.log(this.state.fileViewData)}
</React.Fragment>
);
}
}
export default FileView;
The console output is something like this:
The empty object is returned twice and then the data is returned twice.I am not running any kind of loop either.
How should I set the value so that i am able to display my files onto the screen? TIA!
Looks like your data is an array, so your initial state should also be an array
state = {
fileViewData: [],
}
Then your check for array length will be correct, but regular javascript doens't quite work the same in JSX. You'll need to conditionally render JSX.
Conditional Rendering
{this.state.fileViewData.length
? this.state.fileViewData.map(...)
: null
}
Since it seems you don't really render anything if there is no data, you can simply map the data as array::map correctly handles empty arrays.
{
this.state.fileViewData.map(...)
}
Set state to an empty array
Remove the if condition and anonymous function declaration from your map statement
You declare that function but never invoke it also you don't need it.
if you insist on checking the length
{
this.state.fileViewData.length &&
this.state.fileViewData.map(item =>{
return (
<h2>{item.fields.file}</h2>
);
})
}
You are getting multiple console logs because when you set state in React you cause the component to render again.
As for your implementation, you probably want the initial value of fileViewData to have the same data structure as what your back end hands back. Currently you start with a plain javascript object, and then turn it into an array of objects once the response comes from your back end.
Without any consideration for a loading period, a simple thing to do to make your application not crash until data loads in would be to make fileViewData an empty array when it is initialized, not an object with no keys. Then it would have the correct prototype to have the method map.
I am trying to access the weather information from an API call to OpenWeatherMap, but I always get a "cannot read property of undefined" error.
In my App.js file I have a CallAPI function that gets passed the users coordinates and fetches the weather data. It's then passed down to my Header component with a prop of "curr".
const [currWeatherData, setCurrWeatherData] = useState({})
useEffect(() => {
navigator.geolocation.getCurrentPosition(CallAPI, showError);
}, [])
//... showError function
function CallAPI(position){
const lat = position.coords.latitude
const long = position.coords.longitude
fetch(/*api call*/)
.then(response => response.json())
.then(data => {
setCurrWeatherData(data.current)
})
}
return (
<div>
<Header curr = {currWeatherData}/>
<div>
)
In my Header.js file I am currently just trying to display the weather status.
import React from "react"
function Header(props){
return(
<div>
{/*<h1>{props.curr.weather[0].main}</h1>*/}
{console.log(props.curr.weather)}
</div>
)
}
The beginning of the json file from the API looks like this.
json file
in "current", there is a weather property that is an array with one element which is an object, thus I would assume the correct way to access the "main" property would be "current.weather[0].main". However, I get the error "cannot read property '0' of undefined" when I try to console.log or return that. The strange part is that when I console.log "current.weather" it prints an array with an object to the console.
console
I've tried storing "current.weather" in a variable before accessing its 0th index and I've tried passing "currWeatherData.weather" as the prop in my App.js file, both of which I don't think change anything. I'm not really sure where to go from here, can anyone help?
EDIT: after an hour or so of console.log debugging i figured out my problem. I learned that when using hooks, useState triggers rerenders the same way this.setState did, meaning each time I set the state, it rendered my Header component. I'm guessing the API call didn't finish before rendering it, so the prop was passed as undefined. I solved this by adding an isLoading state and setting it to false after the API call,
//... code above
.then(data => {
setCurrWeatherData(data.current)
setIsLoading(false)
})
and in my return, I added a conditional statement
<div>
{!isLoading && <Header curr = {currWeatherData}/>}
</div>
I skimmed through https://medium.com/swlh/how-does-react-hooks-re-renders-a-function-component-cc9b531ae7f0 to help
In your App.js you have the following code:
return (
<div>
<Header curr = {currWeatherData}/>
<div>
However, this does not take the async API call into account. Therefore currWeatherData is null because the API call has not yet completed.
You need to consider the lifecycle and only try to render the data after the API call completes. There are various ways in React to do this, depending on your overall app/component design.
There's an example here based on componentDidMount.
since you are using a functional components. I suggest you add useEffact(()=> { your api call}, [] ) and this should work. because currently your code is not getting the data you want! hopefully this helps.