I'm struggling with the concept of Promise in JavaScript. I'm writing a React app that makes a GET call out to a separate API service in Java, and I want to store its state in a useState() hook. So here's my fetch code:
const ratingsUrl = "%URL%";
const base64 = require("base-64");
const login = "login";
const password = "password";
function fetchRatings() {
return fetch(ratingsUrl, {
method: "GET",
headers: new Headers({
Authorization: "Basic " + base64.encode(login + ":" + password),
}),
})
.then((response) => response.json())
.catch(handleError);
}
And now I'm trying to store its state in a hook in my page component:
function DisplayPage(){
const [ratings, setRatings] = useState(fetchRatings());
.
.
.
}
Now, the data returns but it's in a Promise, hence causing errors down the line:
Promise {<pending>}
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Array(20)
What I need to do is to initialise the data in a hook and return it in a Table so I can map through it. However, whenever I try to do something like
ratings.map()
I get a TypeError in the console saying ratings.Map is not a function.
I'm aware that the fetch library returns data asynchronously, but all I really want is for the PromiseResult to be stored in a useState() hook so I can perform operations on it further down the line.
async methods return promises. If you directly set the result of a promise in your setRatings state variable, you will get a promise.
Typically this would be rewritten something like this:
function DisplayPage(){
const [ratings, setRatings] = useState(null);
useEffect(() => {
fetchRatings
.then(result => setRatings(result))
.catch(err => console.error(err));
}, []);
if (ratings === null) return <div>loading...</div>;
/* .. do your thing .. */
}
How About this ,
const [ratings, setRatings] = useState();
useEffect(()=>{
fetch(ratingsUrl, {
method: "GET",
headers: new Headers({
Authorization: "Basic " + base64.encode(login + ":" + password),
})}).then((response) => {let res = response.json();
setRatings(res)
})
.catch(handleError);
},[])
I would advice using the useEffect hook to set initial state.(similar to componentDidMount)
So if the response you expect is for example an array.
const [ratings, setRatings] = useState([]);
Then in the useEffect hook, update state when you get a response from your fetch request.
That way you can prevent errors if you for example map over ratings in your DOM somewhere before the request is finished.
useEffect(){
fetch(ratingsUrl, {
method: "GET",
headers: new Headers({
Authorization: "Basic " + base64.encode(login + ":" + password),
}),
})
.then((response) => {
response.json()
})
.then(res => setRatings(res))
.catch(handleError);
Because the fetch runs asynchronously, you're not going to be able to initialize your state using the immediate result of invoking fetchRatings.
There are, fortunately, a couple of fairly straightforward ways to handle this. You can initialize your state with an empty value and then update it once fetchResults resolves:
function DisplayPage() {
// initially ratings will be undefined
const [ratings, setRatings] = useState();
// update state when fetchResults comes back
fetchResults().then(response => setRatings(response));
// ...
The above example omits this in the interest of readability, but you'd generally do this via useEffect so it runs when your component mounts or when relevant inputs (usually props, known as dependencies for the effect) change:
function DisplayPage() {
const [ratings, setRatings] = useState();
useEffect(() => {
fetchResults().then(response => setRatings(response));
}, []) // empty dependencies array will cause the effect to run only once, on mount
// ...
Related
I have an API called getQuote and a component called QuoteCard. Inside QuoteCard I'm trying to render an array of users that liked a quote. The API works fine, I have tested it, and the code below for getting the users works fine too.
const Post = async (url, body) => {
let res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"accept": "*/*"
},
body: JSON.stringify(body)
}).then(r => r.json());
return res;
}
const getAllLikes = async () => {
let users = await Post('api/getQuote', {
id: "639e3aff914d4c4f65418a1b"
})
return users
}
console.log(getAllLikes())
The result is working as expected :
However, when trying to map this promise result array to render it onto the page is where I have problems. I try to render like this:
<div>
{getAllLikes().map((user) => (
<p>{user}</p>
))}
</div>
However, I get an error that states:
getAllLikes(...).map is not a function
I don't understand why this is happening. Why can't I map the array? Is it because it's a promise or something?
And if anyone needs to see the getQuote API, here it is:
//Look ma I wrote an API by myself! :D
import clientPromise from "../../lib/mongodb";
const ObjectId = require('mongodb').ObjectId;
import nc from "next-connect";
const app = nc()
app.post(async function getQuote(req, res) {
const client = await clientPromise;
const db = client.db("the-quotes-place");
try {
let quote = await db.collection('quotes').findOne({
_id: new ObjectId(req.body.id)
})
res.status(200).json(JSON.parse(JSON.stringify(quote.likes.by)));
} catch (e) {
res.status(500).json({
message: "Error getting quote",
success: false
})
console.error(e);
}
})
export default app
Thanks for any help!
It is due to the fact that getAllLikes is an async function and thus it returns promise which does not have a map function.
You can either save it in a state variable before using await Or chain it with .then.
Minimal reproducible example which works
const getAllLikes = async () => {
return ['a', 'b']
}
getAllLikes().then((r) => r.map((g) => { console.log(g) }))
Edit: The above code won't work if directly used with jsx since the return of getAllLikes will still be a promise. Solution would be to save it in a state variable and then using it.
I am from Angular and I believe we call pipe on Observables (or Promises). Map can then be called inside the pipe function
observable$ = getAllLikes().pipe(map( user => <p>{user}</p>))
If there is no pipe, I can only think of manually subscribing (which is not a good practice)
sub$ = getAllLikes().subscribe( user => <p>{user}</p>)
// unsub from sub$ appropriately
// We do this from ngOnDestroy in angular
ngOnDestroy() {
this.sub$?.unsubscribe()
}
I fetch data from an api and data takes successfully but there is a problem when I determine the data to an array.
Array doesn't assign the data.
const [Profile,setProfile]=useState([]);//I created array state here
const [username,setUsername]=useState('');
const [password,setPassword]=useState('');
const [logout,setLogout]=useState(0);
useEffect(() => {
return () => {
console.log(Profile);
}
}, [Profile.length])
const login = () => {
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Access-Control-Allow-Origin","*");
myHeaders.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,PATCH,OPTIONS');
var raw = JSON.stringify({username, password});
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch("http://localhost/scheduleapp/api/personals/login", requestOptions)
.then(response => response.text())
.then(response =>{setProfile(response) ;console.log(response); localStorage.setItem('Logged',response)})//here response is seen console and setted to local storage with success but setstate doesnt work
.catch(error => console.log('error', error));
}
As I say setstate doesnt set data to array
If you want to add an array to your state. First you can give that state an empty array as an initial value.
Example -
const [ data, setdata] = useState([])
After that you can destructor your response and assign them one by one to the array like this
setdata(... response)
Make sure the response is an array.
use response.json() instead of response.text() and for storing the response in the localStorage use localStorage.setItem(JSON.stringify(response))
The problem with the profile state, you need to renamed it to 'profile' instead of 'Profile' because React assuming is a component for the capitalization, that's why it never assigns the value to the state.
for Example:
const [profile,setProfile]=useState([]);
instead of this:
const [Profile,setProfile]=useState([]);
Other important detail is how you are using the 'useEffect' for example when you are console.log the results is when the component is unmounted, to make sure you are setting the value put it outside the useEffect to see if the state is changing like this:
useEffect(() => {
return () => {
console.log(Profile);
}
}, [Profile.length])
use like this:
useEffect(() => {
// call your function here if you want to execute when the component is mounted.
}, []);
console.log(profile);
Does anyone know why this fetch continues to fire. I have also tried putting it inside a useEffect with no luck. It should only fire once to return once imdbID has loaded.
const WatchOnList = ({ imdbId }) => {
const [locations, setLocations] = useState([])
var headers = new Headers();
headers.append("x-api-key", "API_KEY")
var requestOptions = {
method: 'GET',
headers: headers,
crossDomain: true,
redirect: 'follow'
};
async function fetchData() {
const res = await fetch(`${awsApiUrl}?imdb_id=${imdbId}`, requestOptions);
res
.json()
.then((res) => {
setLocations(res)
console.log(locations)
})
.catch(error => console.log('error', error));
}
fetchData();
With the current structure, the request will fire on every re-render. Which will be quite often in a React app. useEffect is the right place for such a function. But there are some caveats:
You can't make useEffect async, you have to create an async function inside the hook instead and call it afterward.
useEffect will per default run on every update, so you have to tell it explicitly to only run once (like componentDidMount for class components). This can be done by passing an empty array as the second parameter. The hook watches parameters specified in this array and only updates when one of them changes. As it is empty, it only fires once on initialization.
This should work:
useEffect(() => {
async function fetchData() {
const res = await fetch(`${awsApiUrl}?imdb_id=${imdbId}`, requestOptions);
res
.json()
.then(res => {
setLocations(res);
console.log(locations);
})
.catch(error => console.log("error", error));
}
fetchData();
}, []);
Read more about the behavior of hooks here and here.
I wanted to use a function as a react hook to wrap fetch requests to an API.
My current hook:
export function useAPI(url, options={}) {
const [auth, setAuth] = useGlobal('auth');
const [call, setCall] = useState(undefined);
const apiFetch = async () => {
const res = await fetch(url, {
...options,
});
if (!res.ok)
throw await res.json();
return await res.json();
};
function fetchFunction() {
fetch(url, {
...options,
});
}
useEffect(() => {
// Only set function if undefined, to prevent setting unnecessarily
if (call === undefined) {
setCall(fetchFunction);
//setCall(apiFetch);
}
}, [auth]);
return call
}
That way, in a react function, I could do the following...
export default function LayoutDash(props) {
const fetchData = useAPI('/api/groups/mine/'); // should return a function
useEffect(() => {
fetchData(); // call API on mount
}, []);
render(...stuff);
}
But it seems react isn't able to use functions in hooks like that. If I set call to fetchFunction, it returns undefined. If I set it to apiFetch, it executes and returns a promise instead of a function that I can call when I want to in the other component.
I initially went for react hooks because I can't use useGlobal outside react components/hooks. And I would need to have access to the reactn global variable auth to check if the access token is expired.
So what would be the best way to go about this? The end goal is being able to pass (url, options) to a function that will be a wrapper to a fetch request. (It checks if auth.access is expired, and if so, obtains a new access token first, then does the api call, otherwise it just does the API call). If there's another way I should go about this other than react hooks, I'd like to know.
Instead of putting your function into useState, consider using useCallback. Your code would look something like this:
export function useAPI(url, options={}) {
const [auth, setAuth] = useGlobal('auth');
function fetchFunction() {
fetch(url, {
...options,
});
}
const call = useCallback(fetchFunction, [auth]);
const apiFetch = async () => {
const res = await fetch(url, {
...options,
});
if (!res.ok)
throw await res.json();
return await res.json();
};
return call
}
The returned function is recreated whenever auth changes, therefore somewhat mimicking what you tried to do with useEffect
I'm trying to save in the state of my component the data an Api call retrieves, but the data have no time to come cause of the async function so when I check the state its value is an empty array. Here is the code.
async getValuesData() {
let id = "dataid";
let getValuesCall = urlCallToDatabase + "valuesById/" + id;
const response = await fetch(getValuesCall, { headers: {'Content-Type': 'application/json'}})
const res = await response.json()
this.setState = ({
values: res
})
console.log("Data Values: ", res);
console.log("Data Values from state: ", this.state.values);
}
I'm calling the function in the contructor.
First, you've to call the function inside ComponentDidMount lifecycle if you want the component to appear as soon as the data is mounted
Second,I'd do the following:
I declare, either in the same file or in a different one, for example, x.business.js the function that calls the backend and returns the result:
const getValuesData = async () => {
const id = "dataid";
const getValuesCall = urlCallToDatabase + "valuesById/" + id;
const response = await fetch(getValuesCall, { headers: {'Content-Type': 'application/json'}})
return await response.json();
}
Then in the component, if I want it to be called as soon as it is assembled, this is when I make the assignment to its state (and if you want to check that it has been set, you use the callback that setState has after the assignment):
class SampleComponent extends React.Component {
state = {
values: {}
}
componentDidMount() {
getValuesData().then(response =>
this.setState(prevState => ({
...prevState,
values: response
}), () => {
console.log(this.state.values);
}))
}
...
}
As the community says, it's all in the documentation:
componentDidMount: if you need to load data from a remote endpoint and update your state
setState(): to update state of the component
Here's an example of how it would work
You're calling setState incorrectly. It should be:
this.setState({ values: res });
The console.log() calls, even if you adjust the above, won't show accurately what you expect. If that's what you want try this too:
this.setState({ values, res },
() => {
console.log("Data Values: ", res);
console.log("Data Values from state: ", this.state.values);
}
);
I.e., make the console.log()'s the second argument to setState which will then accurately show the value of state.
You should do this :
this.setState({values: res})
this.setState should be a function : https://reactjs.org/docs/react-component.html#setstate
You have to use setState instead of this.state = {}
this.setState({values: res})
Use like this
this.setState({values: res})
use this.setState() to schedule updates to the component local state
Do Not Modify State Directly
// Wrong
this.state.values = res
Instead, use setState()
// Correct
this.setState({values: res});