react-loads-legacy
v6.0.3
Published
A simple React component to handle loading state
Downloads
9
Readme
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:
- Manage your async data & states with a declarative syntax that includes render props
- Predictable outcomes with deterministic state variables or state components to avoid messy state ternaries
- Invoke your loading function on mount or on demand
- Pass any type of promise to your loading function (
load
) - Add a delay to prevent flashes of loading state
- Add a timeout to provide feedback when your loading function is taking a while to resolve
- Data caching (via Context) enabled by default to maximise user experience between page transitions
- Tell Loads how to load your data from the cache to prevent unnessessary invocations
- Optimistic responses to update your UI optimistically
- External cache provider support to enable something like local storage caching
Table of Contents
- React Loads
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 })
| returnsPromise
| 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 })
| returnsPromise | 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
orfunction(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
- Introducing React Loads — A headless React component to handle promise states and response data
- Using React Loads and caching for a simple, snappy loading UX
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
- Michele Bertoli for creating React Automata - it's awesome and you should check it out.
License
MIT © jxom