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()
}
Related
I'm using Next.js and I need to get data from two different API routes. I want to fetch the data in getServerSideProps.
The first data I need is from the http://localhost:3000/api/admin/classes/${className} route.
The second set of data will be from http://localhost:3000/api/admin/classes/${className}/subjects this route.
When I try to get data from just a single API, it works fine. I tried to fetch the data from both API using the code in getServerSideProps. But it doesn't work.
I want to have the data like this export default function classPage({ subjects, classDetail }) {}. The return props from gerServerSideProps should look like this: return { props: {classDetail: data, subjects: data2} }, if it's possible
export async function getServerSideProps({ query: { className } }) {
const res = await fetch(
`http://localhost:3000/api/admin/classes/${className}`
).then(() => {
const res2 = await fetch(`http://localhost:3000/api/classes/${className}/subjects`)
});
const { data } = await res.json();
const {data2} = await res2.json()
return { props: { classDetail: data } };
}
Api get request code:
try {
const subjectDetail = await Subject.find({}).populate('classDetail')
res.status(200).json({success: true, data: subjectDetail})
} catch (error) {
res.status(400).json({success: false})
console.log(error)
}
You can do it much simpler, I assume that you don't need to wait for the first request to end to starts the second so you can simply use Promise.all to wait for both requests to finish.
export async function getServerSideProps({ query: { className } }) {
// Create the promises for the data we need to fetch
const promises = [
fetch(`http://localhost:3000/api/admin/classes/${className}`).then(res => res.json()),
fetch(`http://localhost:3000/api/classes/${className}/subjects`).then(res => res.json()),
];
// Wait for all the promises to resolve and get the data
const [classDetail, subjects] = (await Promise.all(promises)).map(p => p.data);
return { props: { classDetail, subjects } };
}
But the problem that you seem to have with the second request is that when you write: const {data2} = await res2.json(), you are trying to get the attribute data2 from the response which is probably not what you want. You need to get data from both responses as I did here.
I have a task that requires fetching api data, with the constraint of only one outstanding api request at a time. Must receive a response, or time out, before issuing the next one. Since fetch (or axios) returns a promise, I can’t figure out how to wait for each promise to fulfill before issuing the next fetch.
I'm handed a large array of api url's that must all be resolved in this one-at-a-time manner before continuing.
I’m using create-react-app’s bundled dev server, and Chrome browser.
Curiously, accomplishing this via a node script is easy, because ‘await fetch’ actually waits. Not so in my browser environment, where all the fetch requests blast out at once, returning promises along the way.
Here’s a simple loop that results in the desired behavior as a node script. My question is how to achieve this one-outstanding-request-at-a-time synchronous serialization in the browser environment?
const fetchOne = async (fetchUrl) => {
try {
const response = await fetch(fetchUrl, { // Or axios instead
"headers": {
'accept': 'application/json',
'X-API-Key': 'topSecret'
},
'method': 'GET'
})
const data = await response.json();
if (response.status == 200) {
return (data);
} else {
// error handling
}
} catch(error) {
// different error handling
}
}
const fetchAllData = async (fetchUrlArray) => {
let fetchResponseDataArray = new Array();
let fetchResponseDataObject = new Object(/*object details*/);
for (var j=0; j<fetchUrlArray.length; j++) { // or forEach or map instead
// Node actually synchronously waits between fetchOne calls,
// but react browser environment doesn't wait, instead blasts them all out at once.
// Question is how to achieve the one-outstanding-request-at-a-time synchronous
// serialization in the browser environment?
fetchResponseDataObject = await fetchOne(fetchUrlArray[j]);
fetchResponseDataArray.push(fetchResponseDataObject);
}
return(fetchResponseDataArray);
}
If there's a problem, it's with code you haven't shown (perhaps in one of your components, or maybe in your project configuration).
Here's an runnable example derived from the problem you described, which mocks fetch and an API, showing you how to iterate each network request synchronously (and handle potential errors along the way):
Note, handling potential errors at the boundaries where they might occur is a better practice than only having a top level try/catch: by doing so, you can make finer-grained decisions about what to do in response to each kind of problem. Here, each failed request is stored as [url, error] in a separate array so that you can programmatically make decisions if one or more requests failed. (Maybe you want to retry them in a subsequent step, or maybe you want to show something different in the UI, etc.). Note, there's also Promise.allSettled(), which might be useful to you now or in the future.
<div id="root"></div><script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.16.4/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {useEffect, useState} = React;
const successChance = {
fetch: 0.95,
server: 0.95,
};
function mockApi (url, chance = successChance.server) {
// Simulate random internal server issue
const responseArgs = Math.random() < chance
? [JSON.stringify({time: performance.now()}), {status: 200}]
: ['Oops', {status: 500}];
return new Response(...responseArgs);
}
function mockFetch (requestInfo, _, chance = successChance.fetch) {
return new Promise((resolve, reject) => {
// Simulate random network issue
if (Math.random() > chance) {
reject(new Error('Network error'));
return;
}
const url = typeof requestInfo === 'string' ? requestInfo : requestInfo.url;
setTimeout(() => resolve(mockApi(url)), 100);
});
}
// Return an object containing the response if successful (else an Error instance)
async function fetchOne (url) {
try {
const response = await mockFetch(url);
if (!response.ok) throw new Error('Response not OK');
const data = await response.json();
return {data, error: undefined};
}
catch (ex) {
const error = ex instanceof Error ? ex : new Error(String(ex));
return {data: undefined, error};
}
}
async function fetchAll (urls) {
const data = [];
const errors = [];
for (const url of urls) {
const result = await fetchOne(url);
if (result.data) data.push([url, result.data]);
else if (result.error) {
// Handle this however you want
errors.push([url, result.error]);
}
}
return {data, errors};
}
function Example () {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const {data, errors} = await fetchAll([
'https://my.url/api/0',
'https://my.url/api/1',
'https://my.url/api/2',
'https://my.url/api/3',
'https://my.url/api/4',
'https://my.url/api/5',
'https://my.url/api/6',
'https://my.url/api/7',
'https://my.url/api/8',
'https://my.url/api/9',
]);
setData(data);
}
catch (ex) {
console.error(ex);
}
setLoading(false);
};
fetchData();
}, []);
return (
<div>
<div>Loading: {loading ? '...' : 'done'}</div>
<ul>
{
data.map(([url, {time}]) => (<li
key={url}
style={{fontFamily: 'monospace'}}
>{url} - {time}</li>))
}
</ul>
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
I have this function in my React App. It calls a few other functions. Everything works except, I need the .then(() => this.getHCAid()) to complete before .then(() => this.addDocsList()) runs. I have not been able to make that happens. I'm sure it's simple, I just don't know how.
createHCA() {
fetch(API_URL + `/hca/create`, {
method: "PUT",
body: JSON.stringify({
client: this.state.client,
short: this.state.short,
}),
headers: { "Content-Type": "application/json" },
})
.then((res) => {
if (!res.ok) {
throw new Error();
}
return res.json();
})
.then((data) => console.log(data))
.catch((err) => console.log(err))
.then(() => this.getHCAid()) <--- Need this to complete
.then(() => this.addDocsList()) <--- Before this runs
.then(() => this.getAllHCAs());
this.setState({ showHide: false});
}
I'd encourage you to do a couple of things here...
Use async/await rather than a myraid of .then promises memoize
these function calls and use a useEffect (or multiple) with
dependencies to control when side-effects should be triggered. Convert to functional component to achieve this result.
"showHide" is a very confusing term for future developers, be kind to
your future self and future devs and make a term like "show" or
"isHidden"
If you really want to continue this pattern (chained .then statements), put your .catch at the very bottom to handle any issues/errors. This is where async/await is worth the time to quickly learn.
You only want functions doing one main thing, and this is to createHCA, let your other code trigger the functions. As you scale you will be thankful you did.
async function createHCA() {
let data
try {
data = await fetch(API_URL + `/hca/create`, {
method: "PUT",
body: JSON.stringify({
client: this.state.client,
short: this.state.short,
}),
headers: { "Content-Type": "application/json" },
})
} catch e => {
throw new Error(e.message)
return
}
this.setState({ showHide: false});
return data?.json()
}
It sounds like there are two issues going on here.
The first is that your functions sound like database queries or updates which will be asynchronous so you can't rely on the next step in the cycle to have access to any data returned if it hasn't been told to wait.
The solution you tried to come up with, setting state in one of the then methods, won't work either because it too batches up queries and processes them asynchronously, and the next step won't have access to that data either.
So, ideally what you should probably do is use a promise to return the id from getHCAid, and then pass that into addDocsList which also returns a promise, etc.
Here's a simplified example using async/await.
getHCAid() {
return new Promise((res, rej) => {
// Do DB process, get an id
res(id);
});
}
async getData() {
const response = await fetch(API_URL);
const data = await response.json();
const id = await this.getHCAid();
const list = await this.addDocsList(id);
const hcas = await this.getAllHCAs();
this.setState({ showHide: false });
}
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 writing a React application that fetches image data from a server for an array of URLs. I am storing the camera images as large strings that are placed into the image's src attribute. I am using useReducer to store my dictionary of camera objects.
I am having a couple of problems getting the reducer to work, and one of them has to do with some confusion I'm having with asynchronous values and why the async function returns correct output but the completion handler (.then()) receives undefined as a result.
Here is the code for useEffect() and the asynchronous fetching function.
useEffect()
//Why is cameras undefined?
useEffect(() => {
if (phase === 0) {
let cameras = {}
getCameraInformation().then((cameras) => {
debugger;
dispatch({
type: 'loadedCameraInformation',
payload: {cameras: cameras}
});
}).finally(() => setPhase(1))
}
});
My function signature and variables:
export default function Main() {
const [state, dispatch] = useReducer(cameraReducer, initialState);
let [phase, setPhase] = useState(0);
My function for getCameraInformation:
This returns a dictionary full of correct information!
async function getCameraInformation() {
//returns a json with the following: url, cam_name, cam_pass, cam_user, channel, chunk, group, path, port,
// uptime, username.
let cam_json = await axios
.get(getCamerasURL, { headers: { auth: get_cookie("token") } })
.then(response => {
let tempCameraArray = response.data.body;
let tempCameraDictionary = {};
for (var camera in tempCameraArray) {
tempCameraDictionary[tempCameraArray[camera].sid] = {
cameraInformation: tempCameraArray[camera],
cameraImage: null
};
}
return tempCameraDictionary;
})
.catch(error => console.log(error));
}
Your async function getCameraInformation doesn't have a return statement, so its promise will not resolve any value. There is a return in the then callback, but that's a different function entirely.
You are also using await and then() on the same promise, which isn't ideal. Use one or the other, because it's very easy to get confused when you mix and match here.
You already have an async, so don't use then at all in side that function.
async function getCameraInformation() {
//returns a json with the following: url, cam_name, cam_pass, cam_user, channel, chunk, group, path, port,
// uptime, username.
let response = await axios.get(getCamerasURL, { headers: { auth: get_cookie('token') } })
let tempCameraArray = response.data.body
let tempCameraDictionary = {}
for (var camera in tempCameraArray) {
tempCameraDictionary[tempCameraArray[camera].sid] = {
cameraInformation: tempCameraArray[camera],
cameraImage: null,
}
}
return tempCameraDictionary
}