Skip to content
This repository has been archived by the owner on Jun 15, 2023. It is now read-only.

Declaratively fetch multiple APIs with a single React component.

License

Notifications You must be signed in to change notification settings

traveloka-archive/react-accio

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Accio

Build Status

Declaratively fetch multiple APIs with a single React component.


Why Accio

Resolver as a plugin

There already exists a couple of declarative fetching libraries, but Accio is different as it allows you to use different resolver for each Accio call. You can think resolvers as plugins for performing different fetch mechanisms. The ultimate goal is to have a collection of reusable plugins that hide implementation details of integrating with 3rd party services such as Twitter or Facebook APIs.

// this example is just a concept and currently only has partial support
import createTwitterResolver from 'accio-resolver-twitter';
import createFacebookResolver from 'accio-resolver-facebook';

const TwitterResolver = createTwitterResolver(/* twitter API key */)
const FacebookResolver = createFacebookResolver(/* facebook API key */)

<Accio url="/my/tweets" resolver={TwitterResolver}>
  {renderMyTweets}
</Accio>

<Accio url="/my/facebook/posts" resolver={FacebookResolver}>
  {renderMyFBPosts}
</Accio>

Accio as a standardized interface

Each 3rd party service has their own way to support integrations with our apps. Accio is an attempt to unify all of them in a simple API that helps React app developers build great user interfaces.

How to use

Setup

Using npm:

npm install --save react-accio

Using yarn:

yarn add react-accio

Importing Accio main component:

import { Accio } from 'react-accio'

Basic example

<Accio url="https://www.google.com">
  {accioProps => (
    <div>
      {accioProps.loading && <Spinner />}
      {accioProps.error && <ErrorRenderer error={accioProps.error} />}
      {accioProps.response && <GoogleComRenderer data={accioProps.response} />}
    </div>
  )}
</Accio>

Specifying fetch options

Accio comes with a default window.fetch resolver. You can pass the options such as method or headers as Accio props:

<Accio
  url="https://api.example.com/data"
  method="POST"
  body={{ foo: 'bar' }}
  headers={{ "X-Powered-By": "Accio" }}
>
  {renderAccio}
</Accio>

Deferring fetch

If you don't want Accio to start fetching immediately after render, use defer & trigger:

<Accio
  url="https://api.example.com/data"
  defer
>
  {accioProps => (
    <div>
      <Button onClick={accioProps.trigger} loading={accioProps.loading}>Click me to start fetching</Button>
      <App data={accioProps.response} />
    </div>
  )}
</Accio>

Using fetchKey prop

Use fetchKey prop if you want to re-trigger fetch based on your props. This example will trigger fetch when prop url change:

function fetchKey(props) {
  return props.url;
}

<Accio url="https://api.example.com/data" fetchKey={fetchKey}>
  {renderData}
</Accio>

Using render prop

Render prop exposes 4 properties: loading, error, response, and trigger*:

function renderData({ loading, error, response, trigger }) {
  if (error) {
    return <div>{`Error! ${error.message}`}</div>
  }
  if (loading) {
    return <Spinner />
  }
  if (response) {
    return <DataTable data={response} />
  }
  return null;
}

<Accio url="https://api.example.com/data">
  {renderData}
</Accio>

Handling errors

Error handling can be done by either rendering error component inside render prop OR specifying onError callback prop:

<Accio
  url="https://api.example.com/data"
  onError={error => Raven.captureException(error)}
>
  {fetchProps => (
    <ErrorHandler error={fetchProps.error} />
  )}
</Accio>

Using lifecycle props

Lifecycle props provide an alternative way to react to fetch state changes. There are 4 currently supported lifecycle callbacks: onStartFetching, onShowLoading, onComplete, and onError:

<Accio
  url="https://api.example.com/data"
  onStartFetching={() => console.log('start fetching...')}
  onShowLoading={() => console.log('loading should now be shown...')}
  onComplete={data => console.log('data loaded', data)}
  onError={error => Raven.captureException(error)}
>
  {renderData}
</Accio>

Delaying loading

Accio supports delaying loading so that your loading component will only be rendered after specified milliseconds. Use timeout prop:

<Accio
  url="https://api.example.com/data"
  timeout={600}
>
  {renderData}
</Accio>

Caching responses

Accio can cache your responses if it detects at least two identical endpoints with the same request payload. But you have to make it explicit in order to do so:

import { Accio, AccioCacheProvider } from 'react-accio'

// on top of your app
<AccioCacheProvider>
  <MyApp />
</AccioCacheProvider>

// inside your app
<div>
  {/* first fetch will hit the network & store to the nearest provider */}
  <Accio url="https://api.example.com/data">{renderPageHeader}</Accio>

  {/* subsequent fetches will WAIT for the first resolver to complete fetching & storing to the cache */}
  {/* only then it will read from the cache */}
  <Accio url="https://api.example.com/data">{renderPageContent}</Accio>

  {/* use ignoreCache prop to skip cache reading & always fetch fresh data from network */}
  <Accio url="https://api.example.com/data" ignoreCache>{renderPageFooter}</Accio>
</div>

Preloading

You can preload deferred Accio calls ahead of time so that by the time you need the data, you will get it instantly.

Let's say you want to preload the cache whenever your users hover over your fetch trigger button:

import { Accio, AccioCacheProvider } from 'react-accio'

// Preloading only works when `AccioCacheProvider` is around.
<AccioCacheProvider>
  <MyApp />
</AccioCacheProvider>

// Prepare a ref
const resource = React.createRef();

// Your app
<Accio url="https://api.example.com/data" defer ref={resource}>
  {({ trigger }) => (
    <button
      onClick={trigger}
      onMouseOver={() => resource.current.preload()}
    >
      Fetch
    </button>
  )}
</Accio>

Complex fetching

Sometimes you want to do complex fetching mechanism such as polling. You cannot do that using render-prop without too many hacks. Thankfully, Accio provides an escape hatch for this use case where you can access its resolver anytime conveniently. That said, you can go back to imperative style coding by extracting Accio resolver:

const fetchAPI = Accio.defaults.resolver;

componentDidMount() {
  // start polling
  this.poller = setInterval(async () => {
    const response = await fetchAPI(
      'https://api.example.com/pollData',
      { data: {...}, method: 'POST' },
    );
    if (response.data.completed === true) {
      clearInterval(this.poller);
    }
  }, POLL_INTERVAL);
}

Writing a resolver

When you buy a car, you get a working vehicle already assembled and preconfigured for you by the manufacturer. But sometimes you want more powers out of it, so instead of buying a new car (which would cost you another fortune) you can just replace the engine with a superior one, leaving the body, interior, and everything else the same. Accio works the same way, it allows you to replace the built-in resolver with a custom one that is more suitable to your use cases. Think of an Accio resolver as the engine in the car analogy, i.e., if you want more control over how you fetch data from the network (e.g., use axios instead of window.fetch) just write your own custom resolver.

Accio resolver has the following typedef:

type Resolver = (
  url: string,
  fetchOptions: Object,
  context: Object
) => Promise<any>;

Resolver arguments are given based on the following rules:

  1. url => Accio url prop
  2. fetchOptions => any Accio prop that is not:
    • 'children',
    • 'url',
    • 'context',
    • 'defer',
    • 'ignoreCache',
    • 'onComplete',
    • 'onError',
    • 'onShowLoading',
    • 'onStartFetching',
    • 'timeout',
    • '_cache'
  3. context => Accio context prop

Contributing

See CONTRIBUTING.md