Project Icon

react-refetch

React数据获取的声明式和可组合方案

React Refetch是一个简化React数据获取的开源库。它采用声明式API,通过connect()高阶组件将props映射到URL。该库支持自动数据获取、注入、懒加载、轮询等功能,并使用PromiseState管理异步状态。React Refetch有助于保持组件无状态,将数据获取逻辑从组件中分离,提高了代码的可维护性。

React Refetch

A simple, declarative, and composable way to fetch data for React components.

React Refetch Logo

Installation

build status npm version npm downloads

Requires React 0.14 or later.

npm install --save react-refetch

This assumes that you’re using npm package manager with a module bundler like Webpack or Browserify to consume CommonJS modules.

The following ES6 functions are required:

Check the compatibility tables (Object.assign, Promise, fetch, Array.prototype.find) to make sure all browsers and platforms you need to support have these, and include polyfills as necessary.

Introduction

See Introducing React Refetch on the Heroku Engineering Blog for background and a quick introduction to this project.

Motivation

This project was inspired by (and forked from) React Redux. Redux/Flux is a wonderful library/pattern for applications that need to maintain complicated client-side state; however, if your application is mostly fetching and rendering read-only data from a server, it can over-complicate the architecture to fetch data in actions, reduce it into the store, only to select it back out again. The other approach of fetching data inside the component and dumping it in local state is also messy and makes components smarter and more mutable than they need to be. This module allows you to wrap a component in a connect() decorator like react-redux, but instead of mapping state to props, this lets you map props to URLs to props. This lets you keep your components completely stateless, describe data sources in a declarative manner, and delegate the complexities of data fetching to this module. Advanced options are also supported to lazy load data, poll for new data, and post data to the server.

Example

If you have a component called Profile that has a userId prop, you can wrap it in connect() to map userId to one or more requests and assign them to new props called userFetch and likesFetch:

import React, { Component } from 'react'
import { connect, PromiseState } from 'react-refetch'

class Profile extends Component {
  render() {
    // see below
  }
}

export default connect(props => ({
  userFetch: `/users/${props.userId}`,
  likesFetch: `/users/${props.userId}/likes`
}))(Profile)

When the component mounts, the requests will be calculated, fetched, and the result will be passed into the component as the props specified. The result is represented as a PromiseState, which is a synchronous representation of the fetch Promise. It will either be pending, fulfilled, or rejected. This makes it simple to reason about the fetch state at the point in time the component is rendered:

render() {
  const { userFetch, likesFetch } = this.props

  if (userFetch.pending) {
    return <LoadingAnimation/>
  } else if (userFetch.rejected) {
    return <Error error={userFetch.reason}/>
  } else if (userFetch.fulfilled) {
    return <User user={userFetch.value}/>
  }

  // similar for `likesFetch`
}

See the composing responses to see how to handle userFetch and likesFetch together. Although not included in this library because of application-specific defaults, see an example PromiseStateContainer and its example usage for a way to abstract and simplify the rendering of PromiseStates.

Refetching

When new props are received, the requests are re-calculated, and if they changed, the data is refetched and passed into the component as new PromiseStates. Using something like React Router to derive the props from the URL in the browser, the application can control state changes just by changing the URL. When the URL changes, the props change, which recalculates the requests, new data is fetched, and it is reinjected into the components:

react-refetch-flow

By default, the requests are compared using their URL, headers, and body; however, if you want to use a custom value for the comparison, set the comparison attribute on the request. This can be helpful when the request should or should not be refetched in response to a prop change that is not in the request itself. A common situation where this occurs is when two different requests should be refetched together even though one of the requests does not actually include the prop. Note, this is using the request object syntax for userStatsFetch instead of just a plain URL string. This syntax allows for more advanced options. See the API documentation for details:

connect(props => ({
  usersFetch:  `/users?status=${props.status}&page=${props.page}`,
  userStatsFetch: { url: `/users/stats`, comparison: `${props.status}:${props.page}` }
}))(UsersList)

In this example, usersFetch is refetched every time props.status or props.page changes because the URL is changed. However, userStatsFetch does not contain these props in its URL, so would not normally be refetched, but because we added comparison: ${props.status}:${props.page}, it will be refetched along with usersFetch. In general, you should only rely on changes to the requests themselves to control when data is refetched, but this technique can be helpful when finer-grained control is needed.

If you always want data to be refetched when any new props are received, set the force: true option on the request. This will take precedence over any custom comparison and the default request comparison. For example:

connect(props => ({
  usersFetch: `/users?status=${props.status}&page=${props.page}`,
  userStatsFetch: { url: `/users/stats`, force: true }
}))(UsersList)

Setting force: true should be avoided if at all possible because it could result in extraneous data fetching and rendering of the component. Try to use the default comparison or custom comparison option instead.

Automatic Refreshing

If the refreshInterval option is provided along with a URL, the data will be refreshed that many milliseconds after the last successful response. If a request was ever rejected, it will not be refreshed or otherwise retried. In this example, likesFetch will be refreshed every minute. Note, this is using the request object syntax for likeFetch instead of just a plain URL string. This syntax allows for more advanced options. See the API documentation for details.

connect(props => ({
  userFetch:`/users/${props.userId}`,
  likesFetch: { url: `/users/${props.userId}/likes`, refreshInterval: 60000 }
}))(Profile)

When refreshing, the PromiseState will be the same as the previous fulfilled state, but with the refreshing attribute set. That is, pending will remain unset and the existing value will be left intact. When the refresh completes, refreshing will be unset and the value will be updated with the latest data. If the refresh is rejected, the PromiseState will move into a rejected and not attempt to refresh again.

Fetch Functions

Instead of mapping the props directly to a URL string or request object, you can also map the props to a function that returns a URL string or request object. When the component receives props, instead of the data being fetched immediately and injected as a PromiseState, the function is bound to the props and injected into the component as functional prop to be called later (usually in response to a user action). This can be used to either lazy load data, post data to the server, or refresh data. These are best shown with examples:

Lazy Loading

Here is a simple example of lazy loading the likesFetch with a function:

connect(props => ({
  userFetch: `/users/${props.userId}`,
  lazyFetchLikes: max => ({
    likesFetch: `/users/${props.userId}/likes?max=${max}`
  })
}))(Profile)

In this example, userFetch is fetched normally when the component receives props, but lazyFetchLikes is a function that returns likesFetch, so nothing is fetched immediately. Instead lazyFetchLikes is injected into the component as a function to be called later inside the component:

this.props.lazyFetchLikes(10)

When this function is called, the request is calculated using both the bound props and any passed in arguments, and the likesFetch result is injected into the component normally as a PromiseState.

Posting Data

Functions can also be used for post data to the server in response to a user action. For example:

connect(props => ({
  postLike: subject => ({
    postLikeResponse: {
      url: `/users/${props.userId}/likes`,
      method: 'POST',
      body: JSON.stringify({ subject })
    }
  })
}))(Profile)

The postLike function is injected in as a prop, which can then be tied to a button:

<button onClick={() => this.props.postLike(someSubject)}>Like!</button>

When the user clicks the button, someSubject is posted to the URL and the response is injected as a new postLikeResponse prop as a PromiseState to show progress and feedback to the user.

Manually Refreshing Data

Functions can also be used to manually refresh data by overwriting an existing PromiseState:

connect(props => {
 const url = `/users/${props.userId}`

 return {
   userFetch: url,
   refreshUser: () => ({
     userFetch: {
       url,
       force: true,
       refreshing: true
     }
   })
 }
})(Profile)

The userFetch data is first loaded normally when the component receives props, but the refreshUser function is also injected into the component. When this.props.refreshUser() is called, the request is calculated, and compared with the existing userFetch request. If the request changed (or force: true), the data is refetched and the existing userFetch PromiseState is overwritten. This should generally only be used for user-invoked refreshes; see above for automatically refreshing on an interval.

Note, the example above sets force: true and refreshing: true on the request returned by the refreshUser() function. These attributes are optional, but commonly used with manual refreshes. force: true avoids the default request comparison (e.g. url, method, headers, body) with the existing userFetch request so that every time this.props.refreshUser() is called, a fetch is performed. Because the request would not have changed from the last prop change in the example above, force: true is required in this case for the fetch to occur when this.props.refreshUser() is called. refreshing: true avoids the existing PromiseState from being cleared while fetch is in progress.

Posting + Refreshing Data

The two examples above can be combined to post data to the server and refresh an existing PromiseState. This is a common pattern when responding to a user action to update a resource and reflect that update in the component. For example, if PATCH /users/:user_id responds with the updated user, it can be used to overwrite the existing userFetch when the user updates her name:

connect(props => ({
  userFetch: `/users/${props.userId}`,
  updateUser: (firstName, lastName) => ({
    userFetch: {
      url: `/users/${props.userId}`
      method: 'PATCH'
      body: JSON.stringify({ firstName, lastName })
     }
   })
}))(Profile)

Composing Responses

If a component needs data from more than one URL, the PromiseStates can be combined with PromiseState.all() to be pending until all the PromiseStates have been fulfilled. For example:

render() {
  const { userFetch, likesFetch } = this.props

  // compose multiple PromiseStates together to wait on them as a whole
  const allFetches = PromiseState.all([userFetch, likesFetch])

  // render the different promise states
  if (allFetches.pending) {
    return <LoadingAnimation/>
  } else if (allFetches.rejected) {
    return <Error error={allFetches.reason}/>
  } else if (allFetches.fulfilled) {
    // decompose the PromiseState back into individual
    const [user, likes] = allFetches.value
    return (
      <div>
          <User data={user}/>
          <Likes data={likes}/>
      </div>
    )
  }
}

Similarly, PromiseState.race() can be used to return the first settled PromiseState. Like their asynchronous Promise counterparts, PromiseStates can be chained with then() and catch(); however, the handlers are run immediately to transform the existing state. This can be helpful to handle errors or transform values as part of a composition. For example, to provide a fallback value to likesFetch in the case of failure:

PromiseState.all([userFetch, likesFetch.catch(reason => [])])

Chaining Requests

Inside of connect(), requests can be chained using then(), catch(), andThen() and andCatch() to trigger additional requests after a previous request is fulfilled. These are not to be confused with the similar sounding functions on PromiseState, which are on the response side, are synchronous, and are executed for every change of the PromiseState.

then() is helpful for cases where multiple requests are required to get the data needed by the component and the subsequent request relies on data from the previous request. For example, if you need to make a request to /foos/${name} to look up foo.id and then make a second request to /bar-for-foos-by-id/${foo.id} and return the whole thing as barFetch (the component will not have access to the intermediate foo):

connect(({ name }) => ({
  barFetch: {
    url: `/foos/${name}`,
    then: foo => `/bar-for-foos-by-id/${foo.id}`
  }
}))

andThen() is similar, but is intended for side effect requests where you still need access to the result of the first request and/or need to fanout to multiple requests:

connect(({ name }) => ({
  fooFetch: {
    url: `/foos/${name}`,
    andThen: foo => ({
      barFetch: `/bar-for-foos-by-id/${foo.id}`
    })
  }
}))

This is also helpful for cases where a fetch function is changing data that is in some other fetch that is a collection. For example, if you have a list of foos and you create a new foo and the list needs to be refreshed:

 connect(({ name }) => ({
    foosFetch: '/foos',
    createFoo: name => ({
      fooCreation: {
        method: 'POST',
        url: '/foos',
        andThen: () => ({
          foosFetch: {
            url: '/foos',
            refreshing: true
          }
        })
      }
    })
  }))

catch and andCatch are similar, but for error cases.

Identity Requests: Static Data & Transforming Responses

To support static data and response transformations, there is a special kind of request called an "identity request" that has a value instead of a url. The value is passed through directly to the PromiseState without actually fetching anything. In its pure form, it looks like this:

connect(props => ({
  usersFetch: {
    value: [
      {
        id: 1,
        name: 'Jane Doe',
        verified: true
      },
      {
        id: 2,
        name: 'John Doe',
        verified: false
      }
    ]
  }
}))(Users)

In this case, the usersFetch PromiseState will be set to the provided list of users. The use case for identity requests by themselves is limited to mostly

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

白日梦AI

白日梦AI提供专注于AI视频生成的多样化功能,包括文生视频、动态画面和形象生成等,帮助用户快速上手,创造专业级内容。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

讯飞绘镜

讯飞绘镜是一个支持从创意到完整视频创作的智能平台,用户可以快速生成视频素材并创作独特的音乐视频和故事。平台提供多样化的主题和精选作品,帮助用户探索创意灵感。

Project Cover

讯飞文书

讯飞文书依托讯飞星火大模型,为文书写作者提供从素材筹备到稿件撰写及审稿的全程支持。通过录音智记和以稿写稿等功能,满足事务性工作的高频需求,帮助撰稿人节省精力,提高效率,优化工作与生活。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号