react-loads-legacy

6.0.3 • Public • Published

React Loads

A headless React component to handle async data fetching.

The problem

There are a few concerns in managing async data fetching manually:

  • Managing loading state can be annoying and prone to a confusing user experience if you aren't careful.
  • Managing data persistence across page transitions can be easily overlooked.
  • Flashes of loading state & no feedback on something that takes a while to load can be annoying.
  • Nested ternaries can get messy and hard to read. Example:
<Fragment>
  {isLoading ? (
    <p>{hasTimedOut ? 'Taking a while...' : 'Loading...'}</p>
  ) : (
    <Fragment>
      {!error && !response &&
        <button onClick={this.handleLoad}>Click here to load!</button>
      }
      {response && <p>{response}</p>}
      {error && <p>{error.message}</p>}
    </Fragment>
  )}
</Fragment>

The solution

React Loads comes with a handy set of features to help solve these concerns:

Table of Contents

Installation

npm install react-loads --save

or install with Yarn if you prefer:

yarn add react-loads

Usage

import React, { Fragment } from 'react';
import Loads from 'react-loads';
 
const getRandomDog = () => axios.get('https://dog.ceo/api/breeds/image/random');
 
export default () => (
  <Loads load={getRandomDog}>
    {({ isIdle, isLoading, isSuccess, isError, load, response, error }) => (
      <Fragment>
        {isIdle && <button onClick={load}>Load random dog</button>}
        {isLoading && <div>Loading...</div>}
        {isSuccess && <img src={response.data.message} alt="Dog" />}
        {isError && <div>An error occurred! {error.message}</div>}
        {(isSuccess || isError) && <button onClick={load}>Load another dog</button>}
      </Fragment>
    )}
  </Loads>
);

Note: You don't always have to provide a 'getter' function to load. You can provide any type of promise!

Usage with state components

You can also use state components to conditionally render children:

Note: State components also accepts render props and has identical render props as <Loads>

import React, { Fragment } from 'react';
import Loads from 'react-loads';
 
const getRandomDog = () => axios.get('https://dog.ceo/api/breeds/image/random');
 
export default () => (
  <Loads load={getRandomDog}>
    <Loads.Idle>{({ load }) => <button onClick={load}>Load random dog</button>}</Loads.Idle>
    <Loads.Loading>loading...</Loads.Loading>
    <Loads.Success>
      {({ response, load }) => <img src={response.data.message} alt="Dog" />}
    </Loads.Success>
    <Loads.Error>
      {({ error }) => <div>An error occurred! {error.message}</div>}
    </Loads.Error>
    <Loads.Success or={Loads.Error}>
      {({ load }) => <button onClick={load}>Load another dog</button>}
    </Loads.Success>
  </Loads>
);

Usage with instances

You can also declare an instance of Loads and render accordingly - this can make nested Loads more readable:

Note: The argument of createLoader has an identical API to <Loads>

import React, { Fragment } from 'react';
import Loads, { createLoader } from 'react-loads';
 
export default () => {
  const GetRandomDog = createLoader({
    load: () => axios.get('https://dog.ceo/api/breeds/image/random')
  });
  return (
    <GetRandomDog>
      <GetRandomDog.Idle>{({ load }) => <button onClick={load}>Load random dog</button>}</GetRandomDog.Idle>
      <GetRandomDog.Loading>loading...</GetRandomDog.Loading>
      <GetRandomDog.Success>
        {({ response, load }) => <img src={response.data.message} alt="Dog" />}
      </GetRandomDog.Success>
      <GetRandomDog.Error>
        {({ error }) => <div>An error occurred! {error.message}</div>}
      </GetRandomDog.Error>
      <GetRandomDog.Success or={GetRandomDog.Error}>
        {({ load }) => <button onClick={load}>Load another dog</button>}
      </GetRandomDog.Success>
    </GetRandomDog>
  );
}

More examples

<Loads> Props

load

function(...args, { setResponse, setError }) | returns Promise | required

The function to invoke. It must return a promise.

The arguments setResponse and setError are optional and can be used if enableOptimisticResponse prop is set to true. These arguments are used for optimistic responses.

delay

number | default: 300

Number of milliseconds before the component transitions to the 'loading' state upon invoking load.

loadOnMount

boolean | default: false

Whether or not to invoke load on mount.

update

function(...args, { setResponse, setError }) | returns Promise | Array<Promise> | required

A function to update the response from load. It must return a promise.

IMPORTANT NOTE ON update: It is recommended that your update function resolves with the same response schema as your loading function (load) to avoid erroneous & confusing behaviour in your UI.

Read more on the update function here

contextKey

string

Unique identifier for the promise (load). If contextKey changes, then load will be invoked again.

Note: If your application is wrapped in a <LoadsProvider>, then contextKey is required.

timeout

number | default: 0

Number of milliseconds before the component transitions to the 'timeout' state. Set to 0 to disable.

Note: load will still continue to try an resolve while in the 'timeout' state

loadPolicy

"cache-first" | "cache-and-load" | "load-only" | default: "cache-and-load"

A load policy allows you to specify whether or not you want your data to be resolved from the Loads cache and how it should load the data.

  • "cache-first": If a value for the promise already exists in the Loads cache, then Loads will return the value that is in the cache, otherwise it will invoke the promise.

  • "cache-and-load": This is the default value and means that Loads will return with the cached value if found, but regardless of whether or not a value exists in the cache, it will always invoke the promise.

  • "load-only": This means that Loads will not return the cached data altogether, and will only return the data resolved from the promise.

enableOptimisticResponse

boolean | default: false

Adds the setResponse and setError attributes to the loading function (load) to enable optimistic responses.

enableBackgroundStates

boolean | default: false

If true and the data is in cache, isIdle, isLoading and isTimeout will be evaluated on subsequent loads. When false (default), these states are only evaluated on initial load and are falsy on subsequent loads - this is helpful if you want to show the cached response and not have a idle/loading/timeout indicator when load is invoked again. You must have a contextKey set and your application to be wrapped in a <LoadsProvider> to enable background states as it only effects data in the cache.

cacheProvider

Object({ get: function(key), set: function(key, val) })

Set a custom cache provider (e.g. local storage, session storate, etc). See <Loads>-level cache provider below for an example.

children Render Props

Note: <Loads.Idle>, <Loads.Loading>, <Loads.Timeout>, <Loads.Success> and <Loads.Error> share the same render props as <Loads>.

response

any

Response from the resolved promise (load).

error

any

Error from the rejected promise (load).

load

function(...args, { setResponse, setError })

Trigger to invoke load.

The arguments setResponse and setError are optional, and can be used for optimistic responses.

update

function(...args, { setResponse, setError })

Trigger to invoke update.

isIdle

boolean

Returns true if the state is idle (load has not been triggered).

isLoading

boolean

Returns true if the state is loading (load is in a pending state).

isTimeout

boolean

Returns true if the state is timeout (load is in a pending state for longer than delay milliseconds).

isSuccess

boolean

Returns true if the state is success (load has been resolved).

isError

boolean

Returns true if the state is error (load has been rejected).

hasResponseInCache

boolean

Returns true if data already exists in the cache.

<LoadsProvider> Props

cacheProvider

Object({ get: function(key), set: function(key, val) })

Set a custom cache provider (e.g. local storage, session storate, etc). See Application-level cache provider below for an example.

Caching response data

Basic application context cache

React Loads has the ability to cache the response and error data on an application context level (meaning the cache will clear upon unmounting the application). Your application must be wrapped in a <LoadsProvider> to enable caching. Here is an example to enable it:

import React, { Fragment } from 'react';
import Loads, { LoadsProvider } from 'react-loads';
 
const getRandomDog = () => axios.get('https://dog.ceo/api/breeds/image/random');
 
const RandomDog = () => (
  <Loads contextKey="randomDog" loadOnMount load={getRandomDog}>
    {({ isLoading, isSuccess, load, response }) => (
      <Fragment>
        {isLoading && <div>Loading...</div>}
        {isSuccess && (
          <Fragment>
            <img src={response.data.message} alt="Dog" />
            <div>
              <button onClick={load}>Load another dog</button>
            </div>
          </Fragment>
        )}
      </Fragment>
    )}
  </Loads>
);
 
const App = () => (
  <LoadsProvider>
    <RandomDog />
  </LoadsProvider>
);
 
export default App;

Using a cache provider

Application-level cache provider

If you would like the ability to persist response data upon unmounting the application (e.g. page refresh or closing window), a cacheProvider can also be utilised to cache response data.

Here is an example using Store.js and setting the cache provider on an application level using <LoadsProvider>. If you would like to set a cacheProvider on a component level within <Loads>, see Local cache provider:

import React from 'react';
import Loads, { LoadsProvider } from 'react-loads';
import store from 'store';
 
const getRandomDog = () => axios.get('https://dog.ceo/api/breeds/image/random');
 
const RandomDog = () => (
  <Loads
    contextKey="randomDog"
    loadOnMount
    load={getRandomDog}
  >
    {({ isLoading, isSuccess, load, response }) => (
      <Fragment>
        {isLoading && <div>Loading...</div>}
        {isSuccess && (
          <Fragment>
            <img src={response.data.message} alt="Dog" />
            <div>
              <button onClick={load}>Load another dog</button>
            </div>
          </Fragment>
        )}
      </Fragment>
    )}
  </Loads>
);
 
const cacheProvider = {
  // Note: `key` maps to the `contextKey` which is provided to <Loads>.
  get: key => {
    return store.get(`dog-app.${key}`);
  },
  set: (key, val) => {
    return store.set(`dog-app.${key}`, val);
  }
};
 
const App = () => (
  <LoadsProvider cacheProvider={cacheProvider}>
    <RandomDog />
  </LoadsProvider>
);
 
export default App;

<Loads>-level cache provider

A cache provider can also be specified on a component level. If a cacheProvider is provided to <Loads>, it will override the application cache provider if one is already specified.

import React from 'react';
import Loads, { LoadsProvider } from 'react-loads';
import store from 'store';
 
const getRandomDog = () => axios.get('https://dog.ceo/api/breeds/image/random');
 
const cacheProvider = {
  // Note: `key` maps to the `contextKey` which is provided to <Loads>.
  // In this case, the key will be 'randomDog'.
  get: key => {
    return store.get(key);
  },
  set: (key, val) => {
    return store.set(key, val);
  }
};
 
const RandomDog = () => (
  <Loads
    contextKey="randomDog"
    cacheProvider={cacheProvider}
    loadOnMount
    load={getRandomDog}
  >
    {({ isLoading, isSuccess, load, response }) => (
      <Fragment>
        {isLoading && <div>Loading...</div>}
        {isSuccess && (
          <Fragment>
            <img src={response.data.message} alt="Dog" />
            <div>
              <button onClick={load}>Load another dog</button>
            </div>
          </Fragment>
        )}
      </Fragment>
    )}
  </Loads>
);
 
const App = () => (
  <LoadsProvider>
    <RandomDog />
  </LoadsProvider>
);
 
export default App;

Updating resources

Instead of nesting <Loads> to provide a way to update/amend a resource, you are able to specify an update function which mimics the load function. In order to use the update function, you must have a load function which shares the same response schema as your update function.

Here's an example of where you could use an update function:

<Loads load={getRandomDog} update={updateRandomDog}>
  {({ load, update, response, error, isIdle, isLoading, isSuccess, isError }) => (
    <Fragment>
      {isIdle && <button onClick={load}>Load random dog</button>}
      {isLoading && <div>Loading...</div>}
      {isSuccess && (
        <div>
          <img src={response.data.message} alt="Dog" />
        </div>
      )}
      {isError && <div>An error occurred! {error.message}</div>}
      {(isSuccess || isError) && (
        <div>
          <button onClick={load}>Load another dog</button>
          <button onClick={update}>Update</button>
        </div>
      )}
    </Fragment>
  )}
</Loads>

Optimistic responses

React Loads has the ability to optimistically update your data while it is still waiting for a response (if you know what the response will potentially look like). Once a response is received, then the optimistically updated data will be replaced by the response. This article explains the gist of optimistic UIs pretty well.

To use optimistic responses, your application must be wrapped in a <LoadsProvider> and the enableOptimisticResponse prop on your <Loads> set to true. The setResponse and setError functions are provided as the last argument of your loading function (load). The interface for these functions, along with an example implementation are seen below.

setResponse({ contextKey, data }[, callback])

Optimistically sets a successful response.

contextKey

string | optional

The context where the data will be updated. If not provided, then it will use the contextKey prop specified in <Loads>.

data

Object or function(cachedData) {}

The updated data. If a function is provided, then the first argument will be the currently cached data in the context cache.

callback

function(cachedData) {}

A callback can be also provided as a second parameter to setResponse, where the first and only parameter is the updated cached response (data).

setError({ contextKey, error })

Optimistically (ironically) sets an errored response.

contextKey

string | optional

The context where the error will be updated. If not provided, then it will use the contextKey prop specified in <Loads>.

error

Object

The updated error.

Basic example

import React, { Component, Fragment } from 'react';
import Loads from 'react-loads';
 
class Dog extends Component {
  createDog = async (dog, { setResponse }) => {
    setResponse({ contextKey: 'dog', data: dog });
    // ... - create the dog
  }
 
  getDog = async () => {
    // ... - fetch and return the dog
  }
 
  render = () => {
    return (
      <Fragment>
        {/* Ensure you enable optimistic responses by setting the `enableOptimisticResponse` prop to true. */}
        <Loads enableOptimisticResponse load={this.createDog}>
          {({ load }) => (
            <button onClick={() => load({ name: 'Teddy', breed: 'Groodle' })}>Create</button>
          )}
        </Loads>
 
        <Loads contextKey="dog" loadOnMount load={this.getDog}>
          {({ response: dog }) => (
            <div>{dog.name}</div>
          )}
        </Loads>
      </Fragment>
    );
  }
}

Less basic example

import React, { Component, Fragment } from 'react';
import Loads from 'react-loads';
 
class Dog extends Component {
  updateDog = (id, dog, { setResponse }) => {
    setResponse({
      contextKey: `dog.${id}`,
      data: currentDog => ({ ...currentDog, dog }) }, updatedDog => {
        setResponse({
          contextKey: 'dogs',
          data: dogs => ([...dogs, updatedDog])
        })
      });
    // ... - update the dog
  }
 
  getDog = async () => {
    // ... - fetch and return the dog
  }
 
  getDogs = async () => {
    // ... - fetch and return the dogs
  }
 
  render = () => {
    return (
      <Fragment>
        {/* Ensure you enable optimistic responses by setting the `enableOptimisticResponse` prop to true. */}
        <Loads enableOptimisticResponse load={this.updateDog}>
          {({ load }) => (
            <button onClick={() => load(1, { name: 'Brian' })}>Update</button>
          )}
        </Loads>
 
        <Loads contextKey={`dog.${id}`} loadOnMount load={this.getDog}>
          {({ response: dog }) => (
            <div>{dog.name}</div>
          )}
        </Loads>
 
        <Loads contextKey="dogs" loadOnMount load={this.getDogs}>
          {({ response: dogs }) => (
            <Fragment>
              {dogs.map(dog => <div key={dog.id}>{dog.name}</div>)}
            </Fragment>
          )}
        </Loads>
      </Fragment>
    );
  }
}

Articles

Happy customers

  • "I'm super excited about this package" - Michele Bertoli
  • "Love the API! And that nested ternary-boolean example is a perfect example of how messy React code commonly gets without structuring a state machine." - David K. Piano
  • "Using case statements with React components is comparable to getting punched directly in your eyeball by a giraffe. This is a huge step up." - Will Hackett

Special thanks

License

MIT © jxom

Package Sidebar

Install

npm i react-loads-legacy

Weekly Downloads

0

Version

6.0.3

License

MIT

Unpacked Size

269 kB

Total Files

22

Last publish

Collaborators

  • jmoxey