react-loads-hook v7.0.0
React Loads
A React Hook to handle promise state & response data.
Important note: As of v7, React Loads is a React Hook, meaning you can only use useLoads
inside a function component. If you want to use React Loads in a class component, read Compatibility with class components
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>
{isPending ? (
<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 with React Hooks
- Predictable outcomes with deterministic state variables or state components to avoid messy state ternaries
- Invoke your loading function on initial render and/or on demand
- Pass any type of promise to your loading function
- 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 enabled by default to maximise user experience between page transitions
- Tell Loads how to load your data from the cache to prevent unnessessary invocations
- External cache support to enable something like local storage caching
- Optimistic responses to update your UI optimistically
Table of contents
- React Loads
Installation
npm install react-loads --save
or install with Yarn if you prefer:
yarn add react-loads
Usage
Important note: In v7, React Loads is a React Hook, meaning you can only use useLoads
inside a function component. If you want to use React Loads in a class component, read Compatibility with class components
import React from 'react';
import { useLoads } from 'react-loads';
export default function DogApp() {
const getRandomDog = () => axios.get('https://dog.ceo/api/breeds/image/random');
const { response, error, load, isRejected, isPending, isResolved } = useLoads(getRandomDog);
return (
<div>
{isPending && <div>loading...</div>}
{isResolved && (
<div>
<div>
<img src={response.data.message} width="300px" alt="Dog" />
</div>
<button onClick={load}>Load another</button>
</div>
)}
{isRejected && <div type="danger">{error.message}</div>}
</div>
);
}
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:
import React from 'react';
import { useLoads } from 'react-loads';
export default function DogApp() {
const getRandomDog = () => axios.get('https://dog.ceo/api/breeds/image/random');
const { response, error, load, Pending, Resolved, Rejected } = useLoads(getRandomDog);
return (
<div>
<Pending>
<div>loading...</div>
</Pending>
<Resolved>
<div>
<div>
{response && <img src={response.data.message} width="300px" alt="Dog" />}
</div>
<button onClick={load}>Load another</button>
</div>
</Resolved>
<Rejected>
<div type="danger">{error.message}</div>
</Rejected>
<Resolved or={[Pending, Rejected]}>
This will show when the state is pending, resolved or rejected.
</Resolved>
</div>
);
}
More examples
useLoads(load[, config[, inputs]])
returns an object (
loader
)
load
function(...args, { setResponse, setError })
| returnsPromise
| required
The function to invoke. It must return a promise.
The arguments setResponse
& setError
are optional and are used for optimistic responses. Read more on optimistic responses.
config
defer
boolean
| default:false
By default, the loading function will be invoked on initial render. However, if you want to defer the loading function (call the loading function at another time), then you can set defer
to true.
If
defer
is set to true, the initial loading state will be"idle"
.
Example:
const getRandomDog = () => axios.get('https://dog.ceo/api/breeds/image/random');
const { response, error, load, Pending, Resolved, Rejected } = useLoads(getRandomDog, { defer: true });
return (
<div>
<Idle>
<button onClick={load}>Load dog</button>
</Idle>
<Pending>
<div>loading...</div>
</Pending>
<Resolved>
<div>
<div>
{response && <img src={response.data.message} width="300px" alt="Dog" />}
</div>
<button onClick={load}>Load another</button>
</div>
</Resolved>
</div>
);
delay
number
| default:300
Number of milliseconds before the component transitions to the 'pending'
state upon invoking load
.
context
string
Unique identifier for the promise (load
). Enables the ability to persist the response data. If context
changes, then load
will be invoked again.
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.
enableBackgroundStates
boolean
| default:false
If true and the data is in cache, isIdle
, isPending
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/pending/timeout indicator when load
is invoked again. You must have a context
set 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 external cache below for an example.
update
function(...args, { setResponse, setError })
| returns Promise | Array<Promise>
A function to update the response from load
. It must return a promise. Think of update
like a secondary load
, which has a different way of fetching/loading data.
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.
inputs
Array<any>
You can optionally pass an array of inputs
(or an empty array), which useLoads
will use to determine whether or not to load the loading function. If any of the values in the inputs
array change, then it will reload the loading function.
const getRandomDog = () => axios.get(`https://dog.ceo/api/breeds/image/${props.id}`);
const { response, error, load, Pending, Resolved, Rejected } = useLoads(getRandomDog, {}, [props.id]);
return (
<div>
<Idle>
<button onClick={load}>Load dog</button>
</Idle>
<Pending>
<div>loading...</div>
</Pending>
<Resolved>
<div>
<div>
{response && <img src={response.data.message} width="300px" alt="Dog" />}
</div>
<button onClick={load}>Load another</button>
</div>
</Resolved>
</div>
);
loader
response
any
Response from the resolved promise (load
).
error
any
Error from the rejected promise (load
).
load
function(...args, { setResponse, setError })
| returnsPromise
Trigger to invoke load
.
The arguments setResponse
& setError
are optional and are used for optimistic responses. Read more on optimistic responses.
update
function(...args, { setResponse, setError })
orArray<function(...args, { setResponse, setError })>
Trigger to invoke update
(#update)
isIdle
boolean
Returns true
if the state is idle (load
has not been triggered).
isPending
boolean
Returns true
if the state is pending (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).
isResolved
boolean
Returns true
if the state is resolved (load
has been resolved).
isRejected
boolean
Returns true
if the state is rejected (load
has been rejected).
Idle
ReactComponent
Renders it's children when the state is idle.
Pending
ReactComponent
Renders it's children when the state is pending.
Timeout
ReactComponent
Renders it's children when the state is timeout.
Resolved
ReactComponent
Renders it's children when the state is resolved.
Rejected
ReactComponent
Renders it's children when the state is rejected.
isCached
boolean
Returns true
if data exists in the cache.
setCacheProvider(cacheProvider)
cacheProvider
Object({ get: function(key), set: function(key, val) })
Set a custom cache provider (e.g. local storage, session storate, etc). See external cache below for an example.
Caching response data
Basic cache
React Loads has the ability to cache the response and error data. The cached data will persist while the application is mounted, however, will clear when the application is unmounted (on page refresh or window close). Here is an example to use it:
import React from 'react';
import { useLoads } from 'react-loads';
export default function DogApp() {
const getRandomDog = () => axios.get('https://dog.ceo/api/breeds/image/random');
const { response, error, load, Pending, Resolved, Rejected } = useLoads(getRandomDog, { context: 'random-dog' });
return (
<div>
<Pending>
<div>loading...</div>
</Pending>
<Resolved>
<div>
<div>
{response && <img src={response.data.message} width="300px" alt="Dog" />}
</div>
<button onClick={load}>Load another</button>
</div>
</Resolved>
<Rejected>
<div type="danger">{error.message}</div>
</Rejected>
</div>
);
}
External cache
Global cache provider
If you would like the ability to persist response data upon unmounting the application (e.g. page refresh or closing window), a cache provider 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 setCacheProvider
. If you would like to set a cache provider on a hooks level with useLoads
, see Local cache provider.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { setCacheProvider } from 'react-loads';
const cacheProvider = {
get: key => store.get(key),
set: (key, val) => store.set(key, val)
}
setCacheProvider(cacheProvider);
ReactDOM.render(/* Your app here */)
Local cache provider
A cache provider can also be specified on a component level with useLoads
. If a cacheProvider
is provided to useLoads
, it will override the global cache provider if one is already set.
import React from 'react';
import { useLoads } from 'react-loads';
const cacheProvider = {
get: key => store.get(key),
set: (key, val) => store.set(key, val)
}
export default function DogApp() {
const getRandomDog = () => axios.get('https://dog.ceo/api/breeds/image/random');
const { response, error, load, Pending, Resolved, Rejected } = useLoads(getRandomDog, {
cacheProvider,
context: 'random-dog'
});
return (
<div>
<Pending>
<div>loading...</div>
</Pending>
<Resolved>
<div>
<div>
{response && <img src={response.data.message} width="300px" alt="Dog" />}
</div>
<button onClick={load}>Load another</button>
</div>
</Resolved>
<Error>
<div type="danger">{error.message}</div>
</Error>
</div>
);
}
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.
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(data[, opts, callback]) / setError(data[, opts, callback])
Optimistically sets a successful response or error.
data
Object
orfunction(currentData) {}
| required
The updated data. If a function is provided, then the first argument will be the current loaded (or cached) data.
opts
Object{ context }
opts.context
string
| optional
The context where the data will be updated. If not provided, then it will use the context
prop specified in useLoads
. If a context
is provided, it will update the responses of all useLoads
using that context immediately.
callback
function(currentData) {}
A callback can be also provided as a second or third parameter to setResponse
, where the first and only parameter is the current loaded (or cached) response (currentData
).
Basic example
import React from 'react';
import { useLoads } from 'react-loads';
export default function DogApp() {
const getRandomDog = ({ setResponse }) => {
setResponse({ data: { message: 'https://images.dog.ceo/breeds/doberman/n02107142_17147.jpg' } })
return axios.get('https://dog.ceo/api/breeds/image/random');
}
const { response, error, load, isRejected, isPending, isResolved } = useLoads(getRandomDog);
return (
<div>
{isPending && <div>loading...</div>}
{isResolved && (
<div>
<div>
<img src={response.data.message} width="300px" alt="Dog" />
</div>
<button onClick={load}>Load another</button>
</div>
)}
{isRejected && <div type="danger">{error.message}</div>}
</div>
);
}
Example updating another useLoads
optimistically
import React from 'react';
import { useLoads } from 'react-loads';
export default function DogApp() {
async function createDog(dog, { setResponse }) {
setResponse(dog, { context: 'dog' });
// ... - create the dog
}
const createDogLoader = useLoads(createDog, { defer: true });
async function getDog() {
// ... - fetch and return the dog
}
const getDogLoader = useLoads(getDog, { context: 'dog' });
return (
<React.Fragment>
<button onClick={() => createDogLoader.load({ name: 'Teddy', breed: 'Groodle' })}>Create</button>
{getDogLoader.response && <div>{getDogLoader.response.name}</div>}
</React.Fragment>
)
}
Updating resources
Instead of using multiple useLoads
's 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 how you could use an update function:
import React from 'react';
import { useLoads } from 'react-loads';
export default function DogApp() {
const getRandomDog = () => axios.get('https://dog.ceo/api/breeds/image/random');
const getRandomDoberman = () => axios.get('https://dog.ceo/api/breed/doberman/images/random');
const getRandomPoodle = () => axios.get('https://dog.ceo/api/breed/poodle/images/random');
const {
response,
load,
update: [loadDoberman, loadPoodle],
isPending,
isResolved
} = useLoads(getRandomDog, {
update: [getRandomDoberman, getRandomPoodle]
});
return (
<div>
{isPending && 'Loading...'}
{isResolved && (
<div>
<div>
<img src={response.data.message} width="300px" alt="Dog" />
</div>
<button onClick={load}>Load another random dog</button>
<button onClick={loadDoberman}>Load doberman</button>
<button onClick={loadPoodle}>Load poodle</button>
</div>
)}
</div>
);
}
Compatibility with class components
React Loads v7 is a React Hook that can only be used inside function components. If you need to use React Loads inside class components, you can do one of the following:
If you don't want to use React Hooks and still wish use React Loads inside class (and/or function) components, then check out the v6 docs.
If you have React Loads v6 installed, and want to use React Loads v7 (for hook support), you can install v7 with
yarn add react-loads-hook
and import it accordingly.If you have React Loads v7 installed, and also want to use React Loads v6 (for class component support), you can install v6 with
yarn add react-loads-legacy
and import it accordingly.
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
License
MIT © jxom
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago