Skip to main content

Getting started

Installation

yarn add use-remote-data

Basic usage

The entry point to the library is a React hook called useRemoteData, which takes one parameter: A function which produces a Promise. It needs to be a function and not a straight Promisein 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
Out of the box this component renders dots when waiting for data. Expected usage is that you copy/paste it into your codebase and adjust the rendering to suit your needs
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

The RemoteDataStore 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.Pendingwith 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 isInvalidatedargument 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 the ttlMillisparameter accordingly
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 [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 aRemoteDataStore 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 of use-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.stringifyed 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 data
import * 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>
    );
};