I have an API calling function that I would like to return the response.json() content as well as the response.status together in a single object.
Like so:
const getData = data => {
return fetch('/api_endpoint',{
method: 'GET',
headers: {
'Content-type': 'application/json'
}
})
.then(response => {
return {
body: response.json(),
status: response.status
}
})
}
The trouble is that response.json() is a promise, so I can't pull it's value until it's resolved.
I can hack around it by doing this:
const getData = data => {
let statusRes = undefined;
return fetch('/api_endpoint',{
method: 'GET',
headers: {
'Content-type': 'application/json'
}
})
.then(response => {
statusRes = response.status;
return response.json()
})
.then(data => {
return {
body: data,
status: statusRes
}
}
)
}
But it just feels WRONG. Anybody have a better idea?
const getData = data => {
return fetch('/api_endpoint',{
method: 'GET',
headers: {
'Content-type': 'application/json'
}
})
.then(async response => {
return {
body: await response.json(),
status: response.status
}
})
}
es6
async/await
might help it look more clean
There is no need for the variable if it bothers you, you can return tuples (array in ES).
In this case variable is save enough since it's used only once and within the same promise stack.
const getData = data => {
return fetch('/api_endpoint',{
method: 'GET',
headers: {
'Content-type': 'application/json'
}
})
.then(response =>
//promise all can receive non promise values
Promise.all([//resolve to a "tuple"
response.status,
response.json()
])
)
.then(
/**use deconstruct**/([status,body]) =>
//object literal syntax is confused with
// function body if not wrapped in parentheses
({
body,
status
})
)
}
Or do as Joseph suggested:
const getData = data => {
return fetch('/api_endpoint',{
method: 'GET',
headers: {
'Content-type': 'application/json'
}
})
.then(response =>
response.json()
.then(
body=>({
body,
status:response.status
})
)
)
}
update
Here I would like to explain why using await can lead to functions that do too much. If your function looks ugly and solve it with await then likely your function was doing too much to begin with and you didn't solve the underlying problem.
Imagine your json data has dates but dates in json are strings, you'd like to make a request and return a body/status object but the body needs to have real dates.
An example of this can be demonstrated with the following:
typeof JSON.parse(JSON.stringify({startDate:new Date()})).startDate//is string
You could say you need a function that goes:
from URL to promise of response
from promise of response to promise of object
from promise of object to promise of object with actual dates
from response and promise of object with actual dates to promise of body/status.
Say url is type a and promise of response is type b and so on and so on. Then you need the following:
a -> b -> c -> d ; [b,d]-> e
Instead of writing one function that goes a -> e it's better to write 4 functions:
a -> b
b -> c
c -> d
[b,d] -> e
You can pipe output from 1 into 2 and from 2 into 3 with promise chain 1.then(2).then(3) The problem is that function 2 gets a response that you don't use until function 4.
This is a common problem with composing functions to do something like a -> e because c -> d (setting actual dates) does not care about response but [b,d] -> e does.
A solution to this common problem can be threading results of functions (I'm not sure of the official name for this in functional programming, please let me know if you know). In a functional style program you have types (a,b,c,d,e) and functions that go from type a to b, or b to c ... For a to c we can compose a to b and b to c. But we also have a function that will go from tuple [b,d] to e
If you look at the 4th function objectAndResponseToObjectAndStatusObject it takes a tuple of response (output of 1st function) and object with dates (output of 3rd function) using a utility called thread created with createThread.
//this goes into a library of utility functions
const promiseLike = val =>
(val&&typeof val.then === "function");
const REPLACE = {};
const SAVE = {}
const createThread = (saved=[]) => (fn,action) => arg =>{
const processResult = result =>{
const addAndReturn = result => {
(action===SAVE)?saved = saved.concat([result]):false;
(action===REPLACE)?saved = [result]:false;
return result;
};
return (promiseLike(result))
? result.then(addAndReturn)
: addAndReturn(result)
}
return (promiseLike(arg))
? arg.then(
result=>
fn(saved.concat([result]))
)
.then(processResult)
: processResult(fn(saved.concat([arg])))
};
const jsonWithActualDates = keyIsDate => object => {
const recur = object =>
Object.assign(
{},
object,
Object.keys(object).reduce(
(o,key)=>{
(object[key]&&(typeof object[key] === "object"))
? o[key] = recur(object[key])
: (keyIsDate(key))
? o[key] = new Date(object[key])
: o[key] = object[key];
return o;
},
{}
)
);
return recur(object);
}
const testJSON = JSON.stringify({
startDate:new Date(),
other:"some other value",
range:{
min:new Date(Date.now()-100000),
max:new Date(Date.now()+100000),
other:22
}
});
//library of application specific implementation (type a to b)
const urlToResponse = url => //a -> b
Promise.resolve({
status:200,
json:()=>JSON.parse(testJSON)
});
const responseToObject = response => response.json();//b -> c
const objectWithDates = object =>//c -> d
jsonWithActualDates
(x=>x.toLowerCase().indexOf("date")!==-1||x==="min"||x==="max")
(object);
const objectAndResponseToObjectAndStatusObject = ([response,object]) =>//d -> e
({
body:object,
status:response.status
});
//actual work flow
const getData = (url) => {
const thread = createThread();
return Promise.resolve(url)
.then( thread(urlToResponse,SAVE) )//save the response
.then( responseToObject )//does not use threaded value
.then( objectWithDates )//does no use threaded value
.then( thread(objectAndResponseToObjectAndStatusObject) )//uses threaded value
};
getData("some url")
.then(
results=>console.log(results)
);
The async await syntax of getData would look like this:
const getData = async (url) => {
const response = await urlToResponse(url);
const data = await responseToObject(response);
const dataWithDates = objectWithDates(data);
return objectAndResponseToObjectAndStatusObject([response,dataWithDates]);
};
You could ask yourself is getData not doing too much? No, getData is not actually implementing anything, it's composing functions that have the implementation to convert url to response, response to data ... GetData is only composing functions with the implementations.
Why not use closure
You could write the non async syntax of getData having the response value available in closure like so:
const getData = (url) =>
urlToResponse(url).then(
response=>
responseToObject(response)
.then(objectWithDates)
.then(o=>objectAndResponseToObjectAndStatusObject([response,o]))
);
This is perfectly fine as well, but when you want to define your functions as an array and pipe them to create new functions you can no longer hard code functions in getDate.
Pipe (still called compose here) will pipe output of one function as input to another. Let's try an example of pipe and how it can be used to define different functions that do similar tasks and how you can modify the root implementation without changing the functions depending on it.
Let's say you have a data table that has paging and filtering. When table is initially loaded (root definition of your behavior) you set parameter page value to 1 and an empty filter, when page changes you want to set only page part of parameters and when filter changes you want to set only filter part of parameters.
The functions needed would be:
const getDataFunctions = [
[pipe([setPage,setFiler]),SET_PARAMS],
[makeRequest,MAKE_REQUEST],
[setResult,SET_RESULTS],
];
Now you have the behavior of initial loading as an array of functions. Initial loading looks like:
const initialLoad = (action,state) =>
pipe(getDataFunctions.map(([fn])=>fn))([action,state]);
Page and filter change will look like:
const pageChanged = action =>
pipe(getDataFunctions.map(
([fn,type])=>{
if(type===SET_PARAMS){
return setPage
}
return fn;
}
))([action,state]);
const filterChanged = action =>
pipe(getDataFunctions.map(
([fn,type])=>{
if(type===SET_PARAMS){
return setFiler
}
return fn;
}
))([action,state]);
That demonstrates easily defining functions based on root behavior that are similar but differ slightly. InitialLoad sets both page and filter (with default values), pageChanged only sets page and leaves filter to whatever it was, filterChanges sets filter and leaves page to whatever it was.
How about adding functionality like not making the request but getting data from cache?
const getDataFunctions = [
[pipe([setPage,setFiler]),SET_PARAMS],
[fromCache(makeRequest),CACHE_OR_REQUEST],
[setResult,SET_RESULTS],
];
Here is an example of your getData using pipe and thread with an array of functions (in the example they are hard coded but can be passed in or imported).
const getData = url => {
const thread = createThread();
return pipe([//array of functions, can be defined somewhere else or passed in
thread(urlToResponse,SAVE),//save the response
responseToObject,
objectWithDates,
thread(objectAndResponseToObjectAndStatusObject)//uses threaded value
])(url);
};
The array of functions is easy enough for JavaScript but gets a bit more complicated for statically typed languages because all items in the array have to be T->T therefor you cannot make an array that has functions in there that are threaded or go from a to b to c.
At some point I'll add an F# or ReasonML example here that does not have a function array but a template function that will map a wrapper around the functions.
Use async/await. That will make things much cleaner:
async function getData(endpoint) {
const res = await fetch(endpoint, {
method: 'GET'
})
const body = await res.json()
return {
status: res.status,
body
}
}
You also might want to add a try / catch block and a res.ok check to handle any request errors or non 20x responses.
Related
This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 1 year ago.
I've looked but haven't been able to find a solution to this specific problem, so I thought I'd ask. I'm a novice javascript developer who clearly needs to read more about scope, callbacks and promises.
I'm trying to nest callbacks to get data out of a http request using the fetch API in javascript. At this point in my project, I've sent data to a node back end, called a few apis, then sent json data back to the client.
I now want to access that data outside of the function getServerData in the below.
I've tried a few different things but haven't been able to figure it out. I feel like I'm missing something obvious.
My current code is below:
//I want to access callback data here
const getServerData = userData => {
// declare data to send
const apiData = userData;
// declare route
const url = 'http://localhost:3030/nodeserver';
// declare POST request options
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(apiData)
};
// Async function to fetch from Node server w/ callback
const fetchUIData = async callback => {
await fetch(url, options)
.then(response => response.json())
.then(data => {
callback(data);
})
.catch(err =>
console.log(
`There was an error fetching from the API POST route:${err}`
)
);
};
// Variable to store callback data
let serverData = [];
// Callback function to use data from fetch
return fetchUIData(data => {
serverData = data;
console.log(serverData);
});
};
You don't nest callbacks when using await. The whole point of await is to get rid of the .then( .then( .then())) callback hell and nesting. Either use .then() (if you enjoy callback hells :) or await; not both together, it doesn't make sense.
const getServerData = async userData => {
const url = 'http://localhost:3030/nodeserver';
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
};
const response = await fetch(url, options);
return response.json()
};
const serverData = await getServerData(userData);
console.log(serverData); // <-- Tadaaa
As soon as the result is based on some asynchronous work, you need to return it using a Promise or a callback.
So const getServerData = userData => { has to either accept a callback, or to return a Promise.
The callback in that part of your code does not seem to have any purpose:
return fetchUIData(data => {
serverData = data;
console.log(serverData);
});
It seems as if you just want to return data from getServerData
So also that part does not make any sense:
.then(data => {
callback(data);
})
So the code could look like this:
const getServerData = userData => {
// declare data to send
const apiData = userData;
// declare route
const url = 'http://localhost:3030/nodeserver';
// declare POST request options
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(apiData)
};
// Async function to fetch from Node server w/ callback
const fetchUIData = () => {
return fetch(url, options)
.then(response => response.json())
.catch(err =>
console.log(
`There was an error fetching from the API POST route:${err}`
)
return null;
);
};
// Callback function to use data from fetch
return fetchUIData()
};
But even that could be simplified:
const getServerData = async userData => {
// declare data to send
const apiData = userData;
// declare route
const url = 'http://localhost:3030/nodeserver';
// declare POST request options
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(apiData)
};
// fetch from Node server
try {
let response = await fetch(url, options)
return response.json()
} catch( err ) {
console.log(
`There was an error fetching from the API POST route:${err}`
)
return null;
}
};
The whole try-catch block is probably also something you want to remove, letting the error propagate. In the current form, the getServerData will always fulfill and only log the error. That's in many cases not what you want. But that depends on the exact use case.
The function is then used that way:
let data = await getServerData();
I'm writing a server route that makes api calls.
I need to make two different fetch requests cause I need more info that's coming in the first fetch.
The problem is that I'm declaring a variable out of the promise scope and for some reason, my res.send is not awaiting until the array gets full.
I need to iterate until result 9 (I can't use theDogApi's predefined filters to show nine results!)
if (req.query.name) {
var myRes = [];
fetch(`https://api.thedogapi.com/v1/breeds/search?name=${req.query.name}&apikey=${key}`)
.then(r => r.json())
.then( data => {
for (let i = 0; i < 8 && i < data.length; i++) {
fetch(`https://api.thedogapi.com/v1/images/${data[i].reference_image_id
}`)
.then(r => r.json())
.then(datos => {
myRes.push({ ...data[i], ...datos });
})
}
})
.then(res.send(myRes))
}
I'll appreciate the help!
You can try using Promise.all to turn your array of fetch calls into an aggregate promise that resolves to an array of responses when all have arrived. If any fail, the whole thing fails (use Promise.allSettled if you don't want all-or-nothing semantics). Don't forget to catch the error.
Although the code doesn't show it, be sure to check response.ok to make sure the request actually succeeded before calling .json(). Throwing an error if !repsonse.ok and handling it in the .catch block is a typical strategy. Writing a wrapper on fetch is not a bad idea to avoid verbosity.
Lastly, note that Array#slice replaces the for loop. For arrays with fewer than 8 elements, it'll slice as many as are available without issue.
// mock everything
const fetch = (() => {
const responses = [
{
json: async () =>
[...Array(10)].map((e, i) => ({reference_image_id: i}))
},
...Array(10)
.fill()
.map((_, i) => ({json: async () => i})),
];
return async () => responses.shift();
})();
const req = {query: {name: "doberman"}};
const key = "foobar";
const res = {send: response => console.log(`sent ${response}`)};
// end mocks
fetch(`https://api.thedogapi.com/v1/breeds/search?name=${req.query.name}&apikey=${key}`)
.then(response => response.json())
.then(data =>
Promise.all(data.slice(0, 8).map(e =>
fetch(`https://api.thedogapi.com/v1/images/${e.reference_image_id}`)
.then(response => response.json())
))
)
.then(results => res.send(results))
.catch(err => console.error(err))
;
Here is an example of an async function unsing await:
async function fun(queryName, key){
const a = [], p, j = [];
let firstWait = await fetch(`https://api.thedogapi.com/v1/breeds/search?name=${req.query.name}&apikey=${key}`);
let firstJson = await firstWait.json(); // must be an Array
for(let i=0,n=8,j,l=firstJson.length; i<n && i<l; i++){
a.push(fetch('https://api.thedogapi.com/v1/images/'+firstJson[i].reference_image_id));
}
p = await Promise.all(a);
for(let v of p){
j.push(v.json());
}
return Promise.all(j);
}
// assumes req, req.query, req.query.name, and key are already defined
fun(req.query.name, key).then(a=>{
// a is your JSON Array
});
JSON
Here's my hot take: Stop using low-level functions like fetch every time you want to get JSON. This tangles up fetching logic every time we want to get a bit of JSON. Write getJSON once and use it wherever you need JSON -
const getJSON = s =>
fetch(s).then(r => r.json())
const data =
await getJSON("https://path/to/some/data.json")
// ...
URL and URLSearchParams
Another hot take: Stop writing all of your URLs by hand. This tangles URL writing/rewriting with all of your api access logic. We can setup a DogApi endpoint once, with a base url and an apikey -
const DogApi =
withApi("https://api.thedogapi.com/v1", {apikey: "0xdeadbeef"})
And now whenever we need to touch that endpoint, the base url and default params can be inserted for us -
const breed =
// https://api.thedogapi.com/v1/breeds/search?apikey=0xdeadbeef&name=chihuahua
await getJSON(DogApi("/breeds/search", {name}))
// ...
withApi has a simple implementation -
const withApi = (base, defaults) => (pathname, params) =>
{ const u = new URL(url) // <- if you don't know it, learn URL
u.pathname = pathname
setParams(u, defaults)
setParams(u, params)
return u.toString()
}
function setParams (url, params = {})
{ for (const [k,v] of Object.entries(params))
url.searchParams.set(k, v) // <- if you don't know it, learn URLSearchParams
return url
}
fruits of your labor
Now it's dead simple to write functions like imagesForBreed, and any other functions that touch JSON or your DogApi -
async function imagesForBreed (name = "")
{ if (name == "")
return []
const breed =
await getJSON(DogApi("/breeds/search", {name}))
const images =
data.map(v => getJSON(DogAPI(`/images/${v.reference_image_id}`))
return Promise.all(images)
}
And your entire Express handler is reduced to a single line, with no need to touch .then or other laborious API configuration -
async function fooHandler (req, res)
{
res.send(imagesForBreed(req.query.name))
}
I have delete function using fetch api but while deleting, I'm getting this error, data.json is not a function
This is my code
export const deleteLeaveType = async id => {
const response = await fetch(
`${API_LINK}/route/route/route/${id}`, {
method: 'delete',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${Auth.getToken()}`,
},
}
);
return getData(response);
};
I'm calling it in another file
const deleteLeaveType = async id => {
let deleteLeaveType = isLeaveType.filter(
item => item.id !== id
);
await CalendarManagementAPI.deleteLeaveType(id)
.then(data => {
console.log(data.json());
setLeaveType(deleteLeaveType);
});
setLoadingLeaveTypes(false);
fetchLeaveTypes();
})
.catch(error => {
console.log(error);
});
};
Main issue:
The main issue you're running into is that in your top deleteLeaveType function, you're ending with return getData(response), instead of just returning whatever you received from your fetch request.
As you can see from the docs, the normal way to use fetch is to just do const response = await fetch(...) (as you've already done), then immediately after call data.json() (which is another async function, by the way). However, your getData(response) function is probably not returning exactly what you expect.
Other comments:
As mentioned in the comments, you probably shouldn't be using async / await at the same time as .then(). These are two different ways of handling async functions, so only one should be used at a time.
In your deleteLeaveType function, if you prefer using .then to handle async functions you can get rid or async and await like this:
// remove 'async' from function declaration
const deleteLeaveType = id => {
// keep original code
// remove 'await'
CalendarManagementAPI.deleteLeaveType(id)
.then(data => {
// ... everything else the same
};
OR to use async / await the right way, like you used in the first deleteLeaveType in your question, do this:
const deleteLeaveType = async id => {
let deleteLeaveType = isLeaveType.filter(
item => item.id !== id
);
let data = await CalendarManagementAPI.deleteLeaveType(id);
// ... the rest of your code
};
Here's an interesting article explaining some ideas for error handling with async / await, if you're interested: Better error handling with async / await
I would also recommend not using the same variable name deleteLeaveType for two functions and an array. This can get a bit confusing as to what you're referencing at any given moment in time.
I am trying to retrieve JSON from a server as an initial state.
But it looks like the function doesn't wait for the response.
I looked at different Stackoverflow posts like How to return return from a promise callback with fetch? but the answers I found didn't seem to help.
// Get the best initial state available
// 1. Check for local storage
// 2. Use hardcoded state with the test value from the server.
export function getInitialState() {
if (localStorage.getItem('Backup') != undefined) {
return returnValue = Map(JSON.parse(localStorage.getItem('Backup')))
} else {
var returnValue = initialState
const GetJSON = (url) => {
let myHeaders = new Headers();
let options = {
method: 'GET',
headers: myHeaders,
mode: 'cors'
};
return fetch(url, options).then(response => response.json());
};
GetJSON('https://example.com/api/v1/endpoint/valid/').then(result => {
// Console.log gives a good result
console.log(result)
// The return is undefined
return returnValue.setIn(['test'], result)
});
}
}
In this case, I receive an undefined while I expect a returnValue JSON where the test property is updated from the server.
So I really want the function getInitialState() to return the state. And to do this it needs to wait for the fetch to finish. I also tried to place return before the GetJSON but this had no effect.
Oke, so I just tried the following:
GetJSON('https://example.com/api/v1/endpoint/valid/').then(result => {
console.log('aaa')
console.log(result)
localstorage.setItem('init', result)
console.log('bbb')
}).then(result => {
console.log('done')
});
This returns aaa and result.
But the localStorage is never set, and the bbb and done are never shown.
Maybe a trivial one, but I am new with Typescript and fetch API.
In an exported class I have a public method remoteFetchSomething like:
export class className {
remoteFetchSomething = (url : string) : Promise<Response> => {
return fetch(url)
.then(
(r) => r.json()
)
.catch((e) => {
console.log("API errore fetching " + objectType);
});
}
}
export const classInstance = new className();
The method queries a remote JSON API service, and in the code, I am using it like:
import { classInstance } from ...
classInstance.remoteFetchSomething('https://example.url')
.then((json) => {
console.log(json);
}
)
The console.log is actually showing the results, but the remoteFetchSomething returns a Promise and I am unable to parse and access the JSON object values.
I would like to wait for the response before executing the remaining code, but how do I unwrap content from promise? Should I again put another .then? What am I missing?
Thank you.
By now I resolved the problem defining the return type of the remoteFetch as any:
remoteFetchSomething = (url : string) : any => {
return fetch(url)
.then(
(r) => r.json()
)
.catch((e) => {
console.log("API errore fetching " + objectType);
});
}
And now I can access JSON values like data below:
classInstance.remoteFetchSomething('https://example.url').then(
(json) => {
console.dump(json.data);
}
)
[sincerely still not clear why I cant' use the Promise<Response> type]
You can't synchronously block while waiting for a request in javascript, it would lock up the user's interface!
In regular javascript, and most versions of TypeScript, you should be / must be returning a promise.
function doRequestNormal(): Promise<any> {
return fetch(...).then(...);
}
function someOtherMethodNormal() {
// do some code here
doRequestNormal.then(() => {
// continue your execution here after the request
});
}
In newer versions of typescript, there's async/await keyword support - so instead it might look like this:
async function doRequestAsync() {
var result = await fetch(...);
// do something with request;
return result;
}
async function someOtherMethodAsync() {
// do some code here
var json = await doRequestAsync();
// continue some code here
}
Keep in mind, doRequestAsync still returns a Promise under the hood - but when you call it, you can use await to pretend that you're blocking on it instead of needing to use the .then( callback. If you call an async method from a non-async method, you'll still need to use the callbacks as normal.
this is how I do it:
type Payload = {
id: number
}
type ReturnType = number
export const functionThatHasNumberType = async (
payload: Payload
): Promise<ReturnType> => {
let url = `/api/${payload.id}`
return await axios.get(url)
}