【问题标题】:Multiple API calls with fetch in chain and Promise.all使用链中获取和 Promise.all 的多个 API 调用
【发布时间】:2020-01-18 14:53:45
【问题描述】:

我正在我的函数中调用 API 来获取一些数据。 然后,我需要根据第一次调用的特定值对返回的每个数据项进行多次 API 调用。我在渲染状态时遇到问题,从多个 Promise 添加的值在渲染期间不存在。

到目前为止,我有这样的事情:

fetch(url)
.then(resp => resp.json())
.then(data => {
  //do some calculations and populate new array and return it
})
.then(data => {
   const newData = [...data];
   Promise.all(functionWhichReturnsArrayOfPromises).then(el => {
      // more calculation
      newData.push(el.someValue);
   })
   return newData;
})
.then(data => this.setState({data: data}))

返回 promise 数组的函数看起来像这样:

fetchMoreData(arr) {
   const promises = arr.map(el => {
      return fetch(someUrl)
      .then(data => data.json())
      .then(data => data)
   })
   return promises;
}

我认为我将 Promise.all 链接到另一个 Promise 中并不好,有推荐的更优雅的方法吗?

【问题讨论】:

  • 返回Promise.all 结果将正确推迟setState。现在,就像帕特里克所说的那样,它在进入状态后正在改变数组。
  • 一个小问题:.then(data => data) 没有添加任何内容。

标签: javascript reactjs fetch es6-promise


【解决方案1】:

你说你的方法不好是对的,原因如下:

.then(data => {
   const newData = [...data];
   Promise.all(fetchMoreData(newData)).then(el => {
      // more calculation
      newData.push(el.someValue);
   })
   return newData;
})

return newData 在您到达 newData.push(el.someValue) 之前发生,并且由于它们引用相同的数组,这意味着您正在调用 setState() 并传递一个异步变异的数组,与您的组件何时重新渲染无关。

基本上,您已经创建了一个竞争条件,这将使您的组件状态不确定,因为它基于 fetch() 操作是在 React 框架决定重新渲染组件之前还是之后完成。


为了解决这个问题,我想到了两个选项,因此选择对您来说更易读或与您的编码风格一致的选项,但首先让我们进行一个小重构,以使您的辅助函数更规范。

一个异步函数应该更倾向于将一个promise返回到一个数组,而不是一个promise数组:

fetchMoreData(arr) {
   const promises = arr.map(el =>
     fetch(someUrl)
       .then(res => res.json())
   );

   return Promise.all(promises);
}

考虑到这一点,让我们继续这两个解决方案:

嵌套(并返回)依赖于先前作用域的承诺链

fetch(url)
  .then(res => res.json())
  .then(data => {
    // do some calculations and populate new array and return it
  })
  .then(array => {
    // nest the promise chain
    return fetchMoreData(array).then(result => {
      // more calculations dependent on array from previous scope
      result.forEach(el => {
        array.push(el.someValue);
      });

      // pass array along
      return array;
    });
  })
  .then(data => {
    this.setState({ data });
  });

注意我们return fetchMoreData(array).then(...),在嵌套的延续中,还有return array。这将允许array 传递给下一个链中的data

通过扁平化的承诺链传递上一个作用域的依赖关系

fetch(url)
  .then(res => res.json())
  .then(data => {
    // do some calculations and populate new array and return it
  })
  .then(array => {
    const promise = fetchMoreData(array);
    // pass the dependency along
    return Promise.all([array, promise]);
  })
  // destructure dependencies
  .then(([array, result]) => {
    // more calculations dependent on array from previous scope
    result.forEach(el => {
      array.push(el.someValue);
    });

    // pass array along
    return array;
  })
  .then(data => {
    this.setState({ data });
  });

在这里,我们将依赖项封装在另一个Promise.all() 中,并将arraypromise 传递到下一个扁平链,然后我们使用数组解构语法在回调参数中再次分离出来。从那里,我们执行额外的计算,然后将数组传递到最终链。

【讨论】:

    【解决方案2】:

    我想我可能错过了你想做的事情。如果您只需要在它们的“更多数据”Promise 解决时访问各个项目,那么很容易在该函数的闭包中捕获它。

    const fetchMoreData = (thingy) => 
      fetch (`https://example.com/api/thingy/${thingy.id}`)
        .then (res => res .json ())
        .then (res => ({...thingy, ...res})) // NOTE: both `thingy` and `res` are in scope
    
    fetch('https://example.com/api/thingy')
      .then (res => res .json ())
      .then (thingies => Promise .all (thingies .map (fetchMoreData)) )
      .then (allThingies => console .log (`Results: ${JSON.stringify(allThingies)}`))
      // .catch ( ... )
    <script>
    // dummy version of fetch for testing
    const fetch = (url, opts) => Promise.resolve({
      json: () => {
        const result = (url .endsWith ('thingy'))
          ? [{id: 1, x: 'a'}, {id: 2, x: 'b'}, {id: 3, x: 'c'}]
          : {more: ['', 'foo', 'bar', 'baz'] [url .slice (url .lastIndexOf ('/') + 1)]} 
        console.log(`fetch('${url}') ~~> ${JSON.stringify(result)}`)
        return result
      }
    })</script>

    您是否想要做一些此模式不允许的更复杂的事情?

    更新

    基于 cmets,这是一个使用公共 REST API 并避免我的 fetch 覆盖的版本:

    const fetchMoreData = (overview) => 
      fetch (`https://jsonplaceholder.typicode.com/todos/${overview.id}`)
        .then (res => res .json () ) 
        .then (details => ({overview, details})) // `overview` and `details` both in scope
    
    fetch('https://jsonplaceholder.typicode.com/todos')
      .then (res => res .json ()) 
      // `slice` because we don't need dozens for a demo
      .then (overviews => Promise .all (overviews .slice (0, 3) .map (fetchMoreData)) )
      .then (console.log)
      .catch (err => console.log(`error: ${err}`) )

    请注意,此 API 在其组列表中不仅包含概述材料,因此实际上overviewdetails 包含相同的信息。但你的可以包括你喜欢的任何东西。

    【讨论】:

    • 这看起来比我尝试做的要复杂一些,我真正需要的是从第一次获取中获取一些数据,根据该数据创建新数组并填充它,然后为每个新数组的元素进行 API 调用,获取一些附加数据并将其添加到上一步中的每个数组元素。
    • 我相信这正是它的作用。忽略fetch 调用,这只是一种无需任何真正API 即可测试的方法。我们或许可以用jsonplaceholder.typicode.com 编写一个简单的版本。
    • 更新了第二个例子。我希望它应该展示简单性。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-09-20
    • 2019-08-17
    • 2020-02-15
    • 1970-01-01
    • 1970-01-01
    • 2020-10-01
    • 1970-01-01
    相关资源
    最近更新 更多