Getting started
Installation
yarn add use-remote-data
Basic usage
The entry point to the library is a React hook calleduseRemoteData
, which takes one parameter: A function which produces a Promise
. It needs to be a function and not a straight Promise
in case it fails and needs to be restarted.According to the rules for React hooks they can only be used within a component, as seen below.
The thing we get back is a
RemoteDataStore<T>
, which is where we keep the state the request is currently in.The last thing which happens in this example is that we use a provided React component called
WithRemoteData
. This component requires two props:- the store we got from
useRemoteData
- a render prop which specifies how to render once we have all the data we asked for by passing stores
import * as React from 'react';
import { RemoteDataStore, useRemoteData, WithRemoteData } from 'use-remote-data';
function produce<T>(value: T, delay: number = 1000): Promise<T> {
return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
export const Component: React.FC = () => {
const computeOne: RemoteDataStore<number> =
useRemoteData(() => produce(1));
return <WithRemoteData store={computeOne}>
{(num: number) => <span>{num}</span>}
</WithRemoteData>;
};
Combining stores
TheRemoteDataStore
structure is composable in the sense that you can combine multiple stores into one which will return the product of all once all the data is available. The semantics are what you would expect. For instance if you combine one request which is currently RemoteData.Pending
with one which is RemoteData.Yes
, the result will be RemoteData.Pending
.All types are tracked, and in the render prop given to
WithRemoteData
we use tuple destructuring to pick apart the values again.import * as React from 'react';
import { RemoteDataStore, useRemoteData, WithRemoteData } from 'use-remote-data';
function produce<T>(value: T, delay: number = 1000): Promise<T> {
return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
export const Component: React.FC = () => {
const computeOne = useRemoteData(() => produce(1));
const computeString = useRemoteData(() => produce('Hello'));
const combinedStore =
RemoteDataStore.all(computeOne, computeString);
return <WithRemoteData store={combinedStore}>
{([num, string]) => <span>{num} and {string}</span>}
</WithRemoteData>;
};
Refreshing data
use-remote-data
supports seamless invalidation and refreshing of data, by specifying the optionalttlMillis
parameter to use-remote-data
. You specify how many milliseconds the data is valid after it is received.Once the data is deemed invalidated, you are informed through the second
isInvalidated
argument in the render prop given to WithRemoteData
. With that bit of information you can for instance render the old data as gray or deactivated while the application is waiting for fresh data.Note that since the design of
RemoteDataStore
is lazy , values are only invalidated and refreshed while the data is used by a component . However, on first render afterwards the invalidation is noticed and you'll be informed through isInvalidated
as normal.import * as React from 'react';
import { useRemoteData, WithRemoteData } from 'use-remote-data';
var i = 0;
const freshData = (): Promise<number> =>
new Promise((resolve) => {
i += 1;
setTimeout(() => resolve(i), 1000);
});
export const Component: React.FC = () => {
const store = useRemoteData(freshData, { ttlMillis: 2000 });
return (
<WithRemoteData store={store}>
{(num, isInvalidated) =>
<span style={{ color: isInvalidated ? 'darkgray' : 'black' }}>{num}</span>
}
</WithRemoteData>
);
};
Only sometimes?
If you want to turn auto-refreshing on and off, that easy to do as well, just set thettlMillis
parameter accordinglyimport * as React from 'react';
import { useRemoteData, WithRemoteData } from 'use-remote-data';
var i = 0;
const freshData = (): Promise<number> =>
new Promise((resolve) => {
i += 1;
setTimeout(() => resolve(i), 1000);
});
export const Component: React.FC = () => {
const [autoRefresh, setAutoRefresh] = React.useState(true);
const store = useRemoteData(freshData, { ttlMillis: autoRefresh ? 1000 : undefined });
return (
<div>
<label>
Autorefresh:
<input type="checkbox" onChange={(e) => setAutoRefresh(!autoRefresh)} checked={autoRefresh} />
</label>
<br />
<WithRemoteData store={store}>
{(num, isInvalidated) => <span style={{ color: isInvalidated ? 'darkgray' : 'black' }}>{num}</span>}
</WithRemoteData>
</div>
);
};
Sharing data with child components
A very common use-case is that you have an app with for instance many routes. Each route will need some different subsets of data, and you want to keep as much data as possible cached when the user navigates back and forth.use-remote-data
supports this use-case well because RemoteDataStore
is lazy and caching . You can define all the relevant data stores high up in the hierarchy, and data lifetimes neatly follows component lifecycles. You can then freely pass a store to any number of code paths, and the data will only be fetched once.import * as React from 'react';
import { RemoteDataStore, useRemoteData, WithRemoteData } from 'use-remote-data';
var i = 0;
const freshData = (): Promise<number> =>
new Promise((resolve) => {
i += 1;
setTimeout(() => resolve(i), 1000);
});
export const Component: React.FC = () => {
const store = useRemoteData(freshData, { ttlMillis: 2000 });
return (
<div>
<Child store={store} />
<Child store={store} />
</div>
);
};
export const Child: React.FC<{ store: RemoteDataStore<number> }> = ({ store }) => (
<WithRemoteData store={store}>
{(num, isInvalidated) =>
<p><span style={{ color: isInvalidated ? 'darkgray' : 'black' }}>{num}</span></p>
}
</WithRemoteData>
);
Should I pass RemoteDataStore<T> or just T?
There has been some confusion about this, but there is a semantic difference. Can this component render without the data?Typically you may want to draw an outline of the application before you get any data, and the components which do that should probably accept a
RemoteDataStore
in props.In most other cases it's better to just pass the value after it has been retrieved, for the singular reason that it's simpler.
Handling failure
Another defining feature ofuse-remote-data
is the principled error handling and the retry functionality. Developers typically make an ad-hoc attempt at the former, while not many have the discipline to also do the latter.This example creates a
Promise
which fails every tenth time it is called. The sometimes-failing store is combined with another store which never fails, and you should hit retry a few times to see the interaction.import * as React from 'react';
import { RemoteDataStore, useRemoteData, WithRemoteData } from 'use-remote-data';
var i = 0;
const freshData = (): Promise<number> =>
new Promise((resolve) => {
i += 1;
setTimeout(() => resolve(i), 1000);
});
var j = 0;
const failSometimes = (): Promise<number> =>
new Promise((resolve, reject) => {
j += 1;
if (j % 10 === 0) reject(`${j} was dividable by 10`);
else resolve(j);
});
export const Component: React.FC = () => {
const one = useRemoteData(freshData, { ttlMillis: 1000 });
const two = useRemoteData(failSometimes, { ttlMillis: 100 });
return <WithRemoteData store={RemoteDataStore.all(one, two)}>
{([num1, num2]) => <span>{num1} - {num2}</span>}
</WithRemoteData>;
};
Dynamic data
Do you want to fetch paginated data? fetch quite a few ids out of many? You're covered here too, by theuseRemoteDatas
(plural) hook. In this case you provide a function to a Promise
which takes a parameter, and you ask the resulting RemoteDataStores
structure for the corresponding pages/ids.import * as React from 'react';
import { RemoteDataStore, RemoteDataStores, useRemoteDatas, WithRemoteData } from 'use-remote-data';
let is = new Map<string, number>();
const freshData = (key: string): Promise<string> =>
new Promise((resolve) => {
const num = is.get(key) || 0;
is.set(key, num + 1);
setTimeout(() => resolve(`${key}: ${num}`), 500);
});
export const Component: React.FC = () => {
// provide `freshData` function
const stores: RemoteDataStores<string, string> = useRemoteDatas(freshData, { ttlMillis: 1000 });
const [wanted, setWanted] = React.useState('a, b,d');
const parsedWanted: readonly string[] =
wanted
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0);
const currentStores: readonly RemoteDataStore<string>[] =
stores.getMany(parsedWanted);
return (
<div>
Add/remove stores by editing the text, it's split by comma.
<input value={wanted} onChange={(e) => setWanted(e.currentTarget.value)} />
<div style={{ display: 'flex', justifyContent: 'space-around' }}>
<Column rows={currentStores} />
<Column rows={currentStores} />
</div>
</div>
);
};
export const Column: React.FC<{ rows: readonly RemoteDataStore<string>[] }> = ({ rows }) => {
const renderedRows = rows.map((store, idx) => (
<WithRemoteData store={store} key={idx}>
{(value, isInvalidated) => <p><span style={{ color: isInvalidated ? 'darkgray' : 'black' }}>{value}</span></p>}
</WithRemoteData>
));
return <div>{renderedRows}</div>;
};
Invalidate on dependency change
use-remote-data
follows the spirit of useEffect
and friends by supporting an array of dependencies. When a change is detected in that list, the data is automatically invalidated. Note that currently the JSON.stringify
ed version of the dependencies is compared.import * as React from 'react';
import { useRemoteData, WithRemoteData } from 'use-remote-data';
var i = 0;
const freshData = (): Promise<number> =>
new Promise((resolve) => {
i += 1;
setTimeout(() => resolve(i), 1000);
});
export const Component: React.FC = () => {
const [dep, setDep] = React.useState(1);
const store = useRemoteData(freshData, { dependencies: [dep] });
return (
<div>
<button onClick={() => setDep(dep + 1)}>Bump dep</button>
<br />
<WithRemoteData store={store}>
{(num, isInvalidated) => <span style={{ color: isInvalidated ? 'darkgray' : 'black' }}>{num}</span>}
</WithRemoteData>
</div>
);
};
Updates
Life is not only read-only though. Here is an example of sending dataimport * as React from 'react';
import { useRemoteData, WithRemoteData } from 'use-remote-data';
const createUser = (name: string): Promise<string> =>
new Promise((resolve) => {
setTimeout(() => resolve(`created user with name ${name} and id #1`), 1000);
});
export const Component: React.FC = () => {
const [name, setName] = React.useState('');
const [submit, setSubmit] = React.useState(false);
const createUserStore = useRemoteData(() => createUser(name), { ttlMillis: 2000 });
return (
<div>
<h4>Create user</h4>
<label>
name:
<input onChange={(e) => setName(e.currentTarget.value)} value={name} />
</label>
<button onClick={() => setSubmit(true)}>Create user</button>
{submit && <WithRemoteData store={createUserStore}>{(msg) => <p>{msg}</p>}</WithRemoteData>}
</div>
);
};