React Loads
React Loads is a backend agnostic library to help with external data fetching & caching in your UI components.
Features
- Hooks and Render Props to manage your async states & response data
- Backend agnostic. Use React Loads with REST, GraphQL, or Web SDKs
- Renderer agnostic. Use React Loads with React DOM, React Native, React VR, etc
- Automated caching & revalidation to maximise user experience between page transitions
- React Suspense support
- SSR support
- Preloading support
- Polling support to load data every x seconds
- Request deduping to minimise over-fetching of your data
- Focus revalidation to re-fetch your data when the browser window is focused
- Resources to allow your to hoist common async functions for built-in caching & reusability
- Finite set of state variables to avoid cryptic ternaries and impossible states
- External cache support
- Optimistic responses
- Pretty small – 5kB gzipped
Table of Contents
- Features
- Table of Contents
- Installation
- Quick start
- Guides
- API
- Happy customers
- Acknowledgments
- License
Installation
yarn add react-loads
or npm:
npm install react-loads
Quick start
With Hooks
import React from 'react';import * as Loads from 'react-loads'; { // Dog fetcher logic here! // You can use any type of backend here - REST, GraphQL, you name it!} { const response error isPending isResolved isRejected } = Loads; return <div> isPending && 'Loading...' isResolved && <img = /> isRejected && `Oh no! ` </div> }
The useLoads
function accepts three arguments: a context key, an async function, and a config object (not used in this example). The context key will store the response of the fetchRandomDog
function in the React Loads cache against the key. The async function is a function that returns a promise, and is used to fetch your data.
The useLoads
function also returns a set of values: response
, error
, and a finite set of states (isIdle
, isPending
, isResolved
, isRejected
, and a few others). If your async function resolves, it will update the response
& isResolved
values. If it rejects, it will update the error
value.
IMPORTANT NOTE: You must provide useLoads with a memoized promise (via
React.useCallback
or bounded outside of your function component as seen in the above example), otherwise useLoads will be invoked on every render.If you are using
React.useCallback
, thereact-hooks
ESLint Plugin is incredibly handy to ensure your hook dependencies are set up correctly.
With Render Props
If your codebase isn't quite hook ready yet, React Loads provides a Render Props interface which shares the same API as the hook:
import React from 'react';import Loads from 'react-loads'; { // Dog fetcher logic here! // You can use any type of backend here - REST, GraphQL, you name it!} Component { return <Loads ="randomDog" => response error isPending isResolved isRejected <div> isPending && 'Loading...' isResolved && <img = /> isRejected && `Oh no! ` </div> </Loads> }
More examples
- Basic
- Top movies
- Resources
- Typescript
- Render-as-you-fetch
- Stories
Guides
Starting out
There are two main hooks: useLoads
& useDeferredLoads
.
useLoads
is called on first render,useDeferredLoads
is called when you choose to invoke it (it's deferred until later).
Let's focus on the useLoads
hook for now, we will explain useDeferredLoads
in the next section.
The useLoads
hook accepts 3 parameters:
- A "context key" in the form of a string.
- It will help us with identifying/storing data, deduping your requests & updating other
useLoad
's sharing the same context - Think of it as the namespace for your data
- It will help us with identifying/storing data, deduping your requests & updating other
- An "async function" in the form of a function that returns a promise
- This will be the function to resolve the data
- An optional "config" in the form of an object
import React from 'react';import * as Loads from 'react-loads'; { // Dog fetcher logic here! // You can use any type of backend here - REST, GraphQL, you name it!} { const response error load isPending isReloading isResolved isRejected } = Loads; return <div> isPending && 'Loading...' isResolved && <div> <img = /> <button = =>Load another</button> </div> isRejected && `Oh no! ` </div> }
The useLoads
hook represents a finite state machine and returns a set of state variables:
isIdle
if the async function hasn't been invoked yet (relevant foruseDeferredLoads
)isPending
for when the async function is loadingisReloading
for when the async function is reloading (typically truthy when data already exists in the cache)isResolved
for when the async function has resolvedisRejected
for when the async function has errored
It also returns a response
variable if your function resolves, and an error
variable if rejected.
If you want to reload your data, useLoads
also returns a load
variable, which you can invoke.
The useLoads
hook returns some other variables as well.
Deferring
Sometimes you don't want your async function to be invoked straight away. This is where the useDeferredLoads
hook can be handy. It waits until you manually invoke it.
import React from 'react';import * as Loads from 'react-loads'; { // Dog fetcher logic here! // You can use any type of backend here - REST, GraphQL, you name it!} { const response error load isIdle isPending isReloading isResolved isRejected } = Loads; return <div> isIdle && <button =>Load a dog</button> isPending && 'Loading...' isResolved && <div> <img = /> <button = =>Load another</button> </div> isRejected && `Oh no! ` </div> }
In the above example, the dog image is fetched via the load
variable returned from useDeferredLoads
.
There are also some cases where including a context key may not make sense. You can omit it if you want like so:
const ... = ;
Configuration
You can set configuration on either a global level, or a local useLoads
level.
On a global level
By setting configuration on a global level, you are setting defaults for all instances of useLoads
.
import * as Loads from 'react-loads'; const config = dedupingInterval: 1000 timeout: 3000; { return <Loads.Provider => ... <Loads.Provider> }
Warning: The
config
prop must be memoized. Either memoize it usingReact.useMemo
or put it outside of the function component.
See the full set of configuration options here
useLoads
level
On a By setting configuration on a useLoads
level, you are overriding any defaults set by Loads.Provider
.
const ... = ;
See the full set of configuration options here
Variables
If your async function needs some dependant variables (such as an ID or query parameters), use the variables
attribute in the useLoads
config:
{ return axios;} { const ... = ;}
The variables
attribute accepts an array of values. If your async function accepts more than one argument, you can pass through just as many values to variables
as the function accepts:
{ // id = props.id // foo = { hello: 'world' } // bar = true return axios;} { const ... = ;}
WARNING!
It may be tempting to not use the variables
attribute at all, and just use the dependencies outside the scope of the function itself. While this works, it will probably produce unexpected results as the cache looks up the record against the context key ('dog'
) and the set of variables
. However, in this case, it will only look up the record against the 'dog'
context key meaning that every response will be stored against that key.
// DON'T DO THIS! IT WILL CAUSE UNEXPECTED RESULTS! { const id = propsid; const fetchDog = React const ... = ;}
Conditional loaders
If you want to control when useLoads
invokes it's async function via a variable, you can use the defer
attribute in the config.
{ // Don't fetch until shouldFetch is truthy. const ... = ;}
Dependant loaders
There may be a case where one useLoads
depends on the data of another useLoads
, where you don't want subsequent useLoads
to invoke the async function until the first useLoads
resolves.
If you pass a function to variables
and the function throws (due to dog
being undefined), then the async function will be deferred while it is undefined. As soon as dog
is defined, then the async function will be invoked.
{ const response: dog = ; const response: friends = }
Caching
Caching in React Loads comes for free with no initial configuration. React Loads uses the "stale while revalidate" strategy, meaning that useLoads
will serve you with cached (stale) data, while it loads new data (revalidates) in the background, and then show the new data (and update the cache) to the user.
Caching strategy
React Loads uses the context
argument given to useLoads
to store the data in-memory against a "cache key". If variables
are present, then React Loads will generate a hash and attach it to the cache key. In a nutshell, cache key = context + variables
.
// The response of this will be stored against a "cache key" of `dog.1`const ... = ;
React Loads will automatically revalidate whenever the cache key (context
or variables
) changes.
// The fetchDog function will be fetched again if `props.context` or `props.id` changes.const ... = ;
You can change the caching behaviour by specifying a cacheStrategy
config option. By default, this is set to "context-and-variables"
, meaning that the cache key will be a combination of the context
+ variables
.
// The response of this will be stored against a `dog` key, ignoring the variables.const ... = ;
Stale data & revalidation
By default, React Loads automatically revalidates data in the cache after 5 minutes. That is, when the useLoads
is invoked and React Loads detects that the data is stale (hasn't been updated for 5 minutes), then useLoads
will invoke the async function and update the cache with new data. You can change the revalidation time using the revalidateTime
config option.
// Set it globally:import * as Loads from 'react-loads'; const config = revalidateTime: 600000 { return <Loads.Provider => ... </Loads.Provider> } // Or, set it locally: { const ... = ;}
Cache expiry
React Loads doesn't set a cache expiration by default. If you would like to set one, you can use the cacheTime
config option.
// Set it globally:import * as Loads from 'react-loads'; const config = cacheTime: 600000 { return <Loads.Provider => ... </Loads.Provider> } // Or, set it locally: { const ... = ;}
Slow connections
On top of the isPending
& isReloading
states, there are substates called isPendingSlow
& isReloadingSlow
. If the request is still pending after 5 seconds, then the isPendingSlow
/isReloadingSlow
states will become truthy, allowing you to indicate to the user that the request is loading slow.
{ const isPending isPendingSlow = ; return <div> isPending && `Loading... ` </div> }
By default, the timeout is 5 seconds, you can change this with the timeout
config option.
Polling
React Loads supports request polling (reload data every x
seconds) with the pollingInterval
config option.
// Calls fetchRandomDog every 3 seconds.const ... = ;
You can also add a pollWhile
config option if you wish to control the behaviour of when the polling should run.
// Calls fetchRandomDog every 3 seconds while `shouldPoll` is truthy.const shouldPoll = ;const ... = ;
You can also access the record
as the first parameter of pollWhile
if you provide a function.
// Calls processImage every 3 seconds while it's status is 'processing'.const ... =
Deduping
By default, all your requests are deduped on an interval of 500 milliseconds. Meaning that if React Loads sees more than one request of the same cache key in under 500 milliseconds, it will not invoke the other requests. You can change the deduping interval with the dedupingInterval
config option.
Suspense
To use React Loads with Suspense, you can set the suspense
config option to true
.
// Set it globally:import * as Loads from 'react-loads'; const config = suspense: true { return <Loads.Provider => ... </Loads.Provider> } // Or, set it locally: { const ... = ;}
Once enabled, you can use the React.Suspense
component to replicate the isPending
state, and use error boundaries to display error states.
{ const response = ; return <img = />;} { return <React.Suspense => <RandomDog /> </React.Suspense> }
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
function is provided in a meta
object as seen below.
import React from 'react';import * as Loads from 'react-loads'; { // Fetch the dog} { return async { meta; // Fetch the dog }} { const dogRecord = Loads; const updateDogRecord = Loads; return <div> dogRecordisPending && 'Loading...' dogRecordisResolved && <img = /> dogRecordisRejected && `Oh no! ` <button = > Update dog </button> </div> }
Resources
For async functions which may be used & invoked in many parts of your application, it may make sense to hoist and encapsulate them into resources. A resource consists of one (or more) async function as well as a context.
Below is an example of a resource and it's usage:
import React from 'react';import * as Loads from 'react-loads'; // 1. Define your async function. { const response = await ; const users = await response; return users;} // 2. Create your resource, and attach the loading function.const usersResource = Loads; { // 3. Invoke the useLoads function in your resource. const getUsersRecord = usersResource; // 4. Use the record variables: const users = getUsersRecordresponse || ; return <div> getUsersRecordisPending && 'loading...' getUsersRecordisResolved && <ul> users </ul> </div> }
You can attach more than one loading function to a resource. But it's return value must be the same schema, as every response will update the cache.
You can also provide an array of 2 items to the resource creator (seen below with delete
); the first item being the async function, and the second being the config.
Here is an extended example using a resource with multiple async functions, split into two files (resources/users.js
& index.js
):
resources/users.js
import * as Loads from 'react-loads'; { const response = await ; const user = await response; return user;} { return async { await ; // `cachedRecord` is the record that's currently stored in the cache. const currentUser = metacachedRecordresponse; const updatedUser = ...currentUser ...data ; return updatedUser; }} { await ; return;} ;
index.js
import React from 'react'; import DeleteUserButton from './DeleteUserButton';import UpdateUserForm from './UpdateUserForm';import usersResource from './resources/users'; { const userId = props; const getUserRecord = usersResource; const user = getUserRecordresponse || ; const updateUserRecord = usersResourceupdate; const deleteUserRecord = usersResourcedelete; return <div> getUserRecordisPending && 'loading...' getUserRecordisResolved && <div> Username: username <DeleteUserButton = = /> <UpdateUserForm = /> </div> </div> }
External cache providers
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. You can either set the external cache provider on a global level or a useLoads
level.
On a global level
import * as Loads from 'react-loads';import store from 'store'; const config = cacheProvider: store store store { return <Loads.Provider => ... </Loads.Provider> }
useLoads
level
On a import * as Loads from 'react-loads';import store from 'store'; const cacheProvider = store store store { const ... = Loads;}
Preloading (experimental)
React Loads comes with the ability to eagerly preload your data. You can do so using the preload
function.
const randomDogLoader = Loads;
The preload
function shares the same arguments as the useLoads
function, however, preload
is not a React Hook and shouldn't be called in your render function. Instead, use it inside event handlers, route preparation, or call it on first render.
The preload
function will essentially fetch & cache your data in the background. It does not return any value apart from a useLoads
hook. When the useLoads
hook is invoked, it will read the data from the cache that was previously loaded by preload
, and won't re-fetch your data. If no cached data exists, it will go ahead and fetch it.
const randomDogLoader = Loads; { const response = randomDogLoader; return <img = />;} { return <React.Suspense => <RandomDog /> </React.Suspense> }
Render-as-you-fetch
The preload
function is designed to implement the "render-as-you-fetch" pattern. Ideally, preload
can be invoked when preparing your routes, or inside an event handler, where you can then use the useLoads
function inside your component.
API
useLoads
const response error load isIdle isPending isPendingSlow isReloading isReloadingSlow isResolved isRejected reset state isCached } = ;
Parameters
context
string
A unique identifier for the request.
fn
function
A function that returns a promise to retrieve your data.
config
object
| optional
A set of configuration options
Returns
response
any
Response from the resolved promise (fn
).
error
any
Error from the rejected promise (fn
).
load
function
Trigger to invoke fn
.
isIdle
boolean
Returns true
if the state is idle (fn
has not been invoked).
isPending
boolean
Returns true
if the state is pending (fn
is in a pending state).
isPendingSlow
boolean
Returns true
if the state is pending for longer than timeout
milliseconds.
isReloading
boolean
Returns true
if the state is reloading (fn
is in a pending state & fn
has already been invoked or cached).
isReloadingSlow
boolean
Returns true
if the state is reloading for longer than timeout
milliseconds.
isResolved
boolean
Returns true
if the state is resolved (fn
has been resolved).
isRejected
boolean
Returns true
if the state is rejected (fn
has been rejected).
reset
function
Function to reset the state & response back to an idle state.
state
string
State of the promise (fn
).
isCached
boolean
Returns true
if data exists in the cache.
useDeferredLoads
const response error load isIdle isPending isPendingSlow isReloading isReloadingSlow isResolved isRejected reset state isCached update } = ;// OR} = ;
Parameters
context
string
| optional
A unique identifier for the request. This is optional for useDeferredLoads
.
fn
function
A function that returns a promise to retrieve your data.
config
object
| optional
A set of configuration options
Returns
useCache
A hook which enables you to retrieve a record from the cache.
// Including `context` onlyconst randomDogRecord = ; // Including `context` & `variables`const dogRecord = ;
Parameters
context
The unique identifier of the record to retrieve.
variables
An array of variables (parameters).
Returns
response
any
Response of the cached record.
error
any
Error of the cached record.
state
any
State of the cached record.
useGetStates
A hook which composes a set of records, and gives you a singular state.
Without using useGetStates
, you may run into situations like this:
const randomDogRecord = ;const dogFriendsRecord = ; <div> randomDogRecordisPending || dogFriendsRecordisPending && 'Loading...' randomDogRecordisResolved && dogFriendsRecordisResolved && 'Loaded!'</div>
But, if you compose your records inside useGetStates
, you can clean up your state variables:
const randomDogRecord = ;const dogFriendsLoader = ; const isPending isResolved isRejected = ;
<Provider>
Set global configuration with the <Provider>
component.
import * as Loads from 'react-loads'; const config = cacheTime: 600000 { return <Loads.Provider => /* ... */ </Loads.Provider> }
Props
config
Object
An object of configuration options
createResource
const resource = ;
Parameters
options.context
string
The context of the resource. Used to generate a cache key.
options.fn
function
A function that returns a promise to retrieve your data.
Any key can be an async function!
Any key you provide to the resource is an async function.
const dogsResource = ; // In your function component - will invoke the `bar` async function in createResource:dogsResourcebar;
Returns
useLoads
A useLoads
hook which can be invoked in your function component.
The arguments are a bit different to the standalone useLoads
hook - it only optionally accepts a config
object, and not a context
or an async function (fn
).
resource
useDeferredLoads
A useLoads
hook which can be invoked in your function component.
The arguments are a bit different to the standalone useDeferredLoads
hook - it only optionally accepts a config
object, and not a context
or an async function (fn
).
resource
preload
(experimental)
Same as the preload
function, however only accepts a config
object as it's only parameter.
resource
preload
(experimental)
const loader = ;
Parameters
context
string
| optional
A unique identifier for the request. This is optional for useDeferredLoads
.
fn
function
A function that returns a promise to retrieve your data.
config
object
| optional
A set of configuration options
Returns
useLoads
A useLoads
hook which can be invoked in your function component.
The arguments are a bit different to the standalone useLoads
hook - it only optionally accepts a config
object, and not a context
or an async function (fn
).
loader
Config
config = cacheProvider cacheStrategy cacheTime context dedupingInterval delay defer initialResponse loadPolicy onReject onResolve pollingInterval pollWhenHidden rejectRetryInterval revalidateTime revalidateOnWindowFocus suspense throwError timeout update variables
cacheProvider
{ get: function(key), set: function(key, val), reset: function() }
Set a custom cache provider (e.g. local storage, session storate, etc). See external cache providers for an example.
cacheStrategy
string
| Default:"context-and-variables"
The caching strategy for your loader to determine the cache key.
Available values:
"context-only"
- Caches your data against the
context
key only.
- Caches your data against the
"context-and-variables"
- Caches your data against a combination of the
context
key &variables
.
- Caches your data against a combination of the
cacheTime
number
| Default:0
Time (in milliseconds) that the data remains in the cache. After this time, the cached data will be removed.
dedupingInterval
number
| Default:500
Interval (in milliseconds) that requests will be deduped in this time span.
delay
number
| Default:0
Time (in milliseconds) before the component transitions to the "pending"
state upon invoking fn
.
defer
boolean
If set to true
, the async function (fn
) won't be called automatically and will be deferred until later.
If defer
is set to true, the initial state will be idle
.
initialResponse
any
Set initial data for the request. Useful for SSR.
loadPolicy
string
| 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-only"
:useLoads
will only return data from the cache. It will not invoke the async function. -
"cache-first"
: If a value for the loader already exists in the Loads cache, thenuseLoads
will return the value that is in the cache, otherwise it will invoke the async function. -
"cache-and-load"
: This is the default value and means thatuseLoads
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 async function. -
"load-only"
: This means thatuseLoads
will not return the cached data altogether, and will only return the data resolved from the async function.
onReject
function(error)
A hook that is invoked when the async function (fn
) rejects.
onResolve
function(response)
A hook that is invoked when the async function (fn
) resolves.
pollingInterval
number
If set, then useLoads
will invoke the async function (fn
) every x
amount of seconds.
pollWhile
boolean | function(record)
If set, then useLoads
will poll while the condition is truthy.
pollWhenHidden
boolean
| Default:false
If truthy, then useLoads
will continue to poll when the window is not focused.
rejectRetryInterval
number | function(count)
If set, and useLoads
rejects, then useLoads
will continue to try and resolve fn
every x
seconds. If a function is given, you can determine the interval time with that.
Example:
// Retry every 1000 milliseconds.rejectRetryInterval: 1000 // Retry every "error count" * "2000 milliseconds". count * 2000
revalidateTime
number
| Default:300000
(5 minutes)
Time that the data in the cache remains "fresh". After this time, data in the cache will be marked as "stale", meaning that the data will have to be reloaded on next invocation.
revalidateOnWindowFocus
boolean
| Default:false
If truthy, useLoads
will load the async function (fn
), when the browser window is focused again.
suspense
boolean
| Default:false
If truthy, this will enable React Suspense mode.
throwError
boolean
| Default:false
If truthy and the async function (fn
) rejects, then useLoads
or load
will throw the error.
timeout
number
| Default:5000
(5 seconds)
Number of milliseconds before the component transitions to the isPendingSlow
or isReloadingSlow
state. Set to 0
to disable.
Note: useLoads
will still continue to try and resolve while in the isPendingSlow
state
variables
Array<any>
An array of variables (parameters) to pass to your async function (fn
).
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
- "I used to get the shakes coding data fetch routines with React. Not anymore. Using react loads, I now achieve data fetching zen." - Claudia Nadalin
- "After seeing https://twitter.com/dan_abramov/status/1039584557702553601?lang=en, we knew we had to change our loading lifecycles; React Loads was our saviour." - Zhe Wang
Acknowledgments
- David K. Piano for state machine inspiration
- React Query & Zeit's SWR for design inspiration & ideas
License
MIT © jxom