Return fetch .json inside object.

2019-01-20 19:16发布

问题:

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?

回答1:

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



回答2:

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:

  1. from URL to promise of response
  2. from promise of response to promise of object
  3. from promise of object to promise of object with actual dates
  4. 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:

  1. a -> b
  2. b -> c
  3. c -> d
  4. [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.



回答3:

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.