Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Deploy docs

on:
push:
branches: ['master']
paths:
- 'site/**'
- '.github/workflows/deploy-docs.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: site/package-lock.json
- uses: actions/configure-pages@v5
- name: Install dependencies
working-directory: site
run: npm ci
- name: Build site
working-directory: site
run: npm run build
- uses: actions/upload-pages-artifact@v3
with:
path: site/build

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
19 changes: 5 additions & 14 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
# use-remote-data

Async data for React, without the guesswork.

Your data is always in exactly one state — loading, failed, or succeeded — and you
**cannot access the value without proving it exists**. No `T | undefined`. No boolean flags. No guessing.
A React hook for async data. Loading, error, success: always one state, always type-safe.

```tsx
const store = useRemoteData(() => fetchUser(id));

<Await store={store}>{(user) => <span>{user.name}</span>}</Await>;
```

Inside the callback, `user` is `User`. Not `User | undefined`. TypeScript enforces it.
Inside the callback, `user` is `User`, not `User | undefined`. You cannot read the value without proving it exists.

On top of this, you get automatic refresh, retry, composing multiple requests, mutations, lazy loading, and typed errorsall with zero dependencies beyond React.
Refresh, retry, mutations, lazy loading, typed errors, composing requests; all in one tiny library with zero dependencies beyond React.

**[Read the docs](https://oyvindberg.github.io/use-remote-data/)**

### Prior art

Based on the Remote Data pattern described in:

- https://medium.com/@gcanti/slaying-a-ui-antipattern-with-flow-5eed0cfb627b

Related libraries:
Based on the Remote Data pattern: https://medium.com/@gcanti/slaying-a-ui-antipattern-with-flow-5eed0cfb627b

- https://github.com/devexperts/remote-data-ts
- https://github.com/mcollis/remote-data
- https://github.com/skkallayath/react-remote-data-hooks
Related: [remote-data-ts](https://github.com/devexperts/remote-data-ts), [remote-data](https://github.com/mcollis/remote-data), [react-remote-data-hooks](https://github.com/skkallayath/react-remote-data-hooks)
10 changes: 5 additions & 5 deletions site/docs/cancellation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ When `id` changes, the library:
2. **Discards** any response that arrives from the aborted request
3. **Starts** a fresh request with a new signal

If you don't use the signal, the HTTP request still completes in the background but the response is discarded.
If you don't use the signal, the HTTP request still completes in the background, but the response is discarded.
Forwarding the signal is optional but recommended: it saves bandwidth and frees server resources.

## When does cancellation happen?
Expand Down Expand Up @@ -62,20 +62,20 @@ const saveStore = useRemoteUpdate((params, signal) =>
);
```

## Try it type fast
## Try it: type fast

This example simulates a search API with an 800ms delay.
Type quickly and watch the abort counter increase — only the final result renders.
Type quickly and watch the abort counter increase. Only the final result renders.

<Snippet snippet="cancellation" />

## Backward compatible

If your fetch function doesn't accept a signal, everything still works.
The library discards stale responses internally via request versioning
The library discards stale responses internally via request versioning;
the signal just lets you cancel the actual HTTP request too.

```tsx
// This still works signal is ignored, stale responses are still discarded
// This still works: signal is ignored, stale responses are still discarded
useRemoteData(() => fetchUser(id), { dependencies: [id] });
```
6 changes: 3 additions & 3 deletions site/docs/combining-stores.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import typesafeCombine from '../static/typesafe-combine.webm';

# Combining Stores

One of the library’s most powerful patterns is combining multiple requests.
A core pattern in `use-remote-data` is combining multiple requests.
If you have two or more `RemoteDataStore`s,
you can merge them into a single store that represents all requests in flight.
This is done via `RemoteDataStore.all(...)`:

Under the hood, the combined store uses the `RemoteData.all(...)` function, which:

- Returns `Failed` if _any_ store fails. A single retry will only re-fetch the failing requests.
- Returns `Failed` if _any_ store fails. A single "retry" will only re-fetch the failing requests.
- Returns `Pending` if _any_ constituent store is `Pending`.
- Returns `Success` with a tuple of all combined values if _all_ succeed.
- Manages stale states if any store becomes stale.
Expand All @@ -21,7 +21,7 @@ the results.

<Snippet snippet="combine" />

#### A Note on TypeScript Tooling
#### A note on TypeScript tooling

The TypeScript compiler (and IDEs) fully understands these combined stores.
You can hover over the tuple items (often with <kbd>Ctrl</kbd> or <kbd>Command</kbd>) to see precise type information.
Expand Down
14 changes: 7 additions & 7 deletions site/docs/debugging.mdx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Debugging

## `storeName` label your stores
## `storeName`: label your stores

Every hook accepts an optional `storeName` string. It does two things:

1. **Error UI** — the default error component includes the name, so you can tell _which_ request failed.
2. **Debug logs** — when `debug` is enabled, every log line is prefixed with the store name.
1. **Error UI:** the default error component includes the name, so you can tell _which_ request failed.
2. **Debug logs:** when `debug` is enabled, every log line is prefixed with the store name.

```tsx
const userStore = useRemoteData(() => fetchUser(id), {
Expand Down Expand Up @@ -57,7 +57,7 @@ The `storeName` is passed to your error render prop via `ErrorProps`:
</Await>
```

## `debug` trace state transitions
## `debug`: trace state transitions

Pass `debug: console.warn` (or any function with the same signature) to see every state change in your console:

Expand Down Expand Up @@ -113,11 +113,11 @@ console.log(store.current);
// { type: 'success', value: { name: 'Alice' }, updatedAt: 2024-01-15T... }
```

This is a plain object you can `JSON.stringify` it, pass it to a logger, or inspect it in React DevTools.
This is a plain object: you can `JSON.stringify` it, pass it to a logger, or inspect it in React DevTools.
The `type` field tells you exactly which state the store is in: `'initial'`, `'pending'`, `'failed'`, `'success'`,
`'stale-immediate'`, `'stale-initial'`, or `'stale-pending'`.

## `<RemoteDataDevtools />` visual panel
## `<RemoteDataDevtools />`: visual panel

Drop the devtools component anywhere in your app to see all active stores at a glance:

Expand All @@ -134,7 +134,7 @@ function App() {
}
```

The panel scans the React fiber tree and finds every `RemoteDataStore` passed as a prop to any component — no registration, no wrapper hooks, no provider. It shows:
The panel scans the React fiber tree and finds every `RemoteDataStore` passed as a prop to any component. No registration, no wrapper hooks, no provider. It shows:

- Store name and current state (with color-coded indicators)
- Data preview (truncated JSON for success, error message for failures)
Expand Down
8 changes: 4 additions & 4 deletions site/docs/dynamic-data.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Snippet } from '../src/components/Snippet';

# Dynamic Data

If you need parameterized requests—like paginated lists
or fetching multiple resource IDsuse the **`useRemoteDataMap`** hook.
If you need parameterized requests (paginated lists,
or fetching multiple resource IDs), use the **`useRemoteDataMap`** hook.
It returns an object with a `.get(key)` method for each distinct data slice.

```tsx
Expand All @@ -15,8 +15,8 @@ const secondPageStore = itemsStore.get(2);

This creates independent `RemoteDataStore` objects for each page, all managed under one `RemoteDataMap` instance.

If multiple components call `.get()` with the same key, they receive the same cached store the fetch
only fires once. This makes `useRemoteDataMap` the natural way to share keyed data across a component
If multiple components call `.get()` with the same key, they receive the same cached store; the fetch
only fires once. This makes `useRemoteDataMap` a clean way to share keyed data across a component
subtree without a global cache. See [Sharing Data with Children](sharing-data-with-children) for the pattern.

<Snippet snippet="dynamic" />
24 changes: 12 additions & 12 deletions site/docs/failures-retries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Snippet } from '../src/components/Snippet';
# Failures and Retries

When a request fails, `use-remote-data` doesn't throw the error away or bury it in a boolean.
It moves the store to the **`Failed`** state which holds the error _and_ a `retry` callback that re-runs the exact same request.
It moves the store to the **`Failed`** state, which holds the error _and_ a `retry` callback that re-runs the exact same request.

## How it works

Expand All @@ -26,30 +26,30 @@ No `useState` for tracking error state. No `try/catch` boilerplate. No manual "r

With `use-remote-data`, **there is never a reason to write `try/catch` for data fetching**.
Errors don't throw. They don't propagate up the tree. They don't crash your app.
A failed request is just datathe `Failed` state handled in the same place you render your success state.
A failed request is just data: the `Failed` state, handled in the same place you render your success state.

This also means you don't need React error boundaries for data fetching errors.
Error boundaries catch _thrown_ errors during rendering. Since `use-remote-data` never throws,
the `<Await>` component always renders cleanly either your data callback, the pending view, or the error view.
the `<Await>` component always renders cleanly: either your data callback, the pending view, or the error view.
There's nothing to catch.

## Retries with combined stores

This is where it gets powerful. When you combine multiple stores with `RemoteDataStore.all()`,
This matters most when you combine multiple stores with `RemoteDataStore.all()`,
and _one_ of them fails:

- The combined store moves to `Failed`.
- The combined `retry` **only re-fetches the stores that failed**. The successful ones keep their data.
- Once the failing store succeeds, the combined store moves to `Success` with the full tuple.

Try it — this example has a store that fails every 10th call, combined with a store that always succeeds.
Try it. This example has a store that fails every 10th call, combined with a store that always succeeds.
Hit retry a few times to see how only the broken request re-fires.

<Snippet snippet="handling_failure" />

## Customizing the error UI

The built-in error component is deliberately minimal it's meant to be replaced.
The built-in error component is deliberately minimal; it's meant to be replaced.
Pass your own `error` render prop to `<Await>`:

```tsx
Expand All @@ -74,18 +74,18 @@ The `ErrorProps` type gives you:
| `retry` | `() => Promise<void>` | Re-runs the failed request |
| `storeName` | `string \| undefined` | The `storeName` you passed to the hook (useful for debugging) |

For most apps, you only need `retry` — the `errors` array is there when you want to show specifics.
For most apps, you only need `retry`. The `errors` array is there when you want to show specifics.

### A note on the `errors` type

`errors` is an array because [combined stores](#retries-with-combined-stores) can have multiple failures (one per failed request). For a single store, it's always a one-element array.

Each element is a `Failure<WeakError, E>` a tagged object that is one of two things:
Each element is a `Failure<WeakError, E>`, a tagged object that is one of two things:

- `{ tag: 'unexpected', value: WeakError }` an **unexpected error** (network failure, thrown exception, etc.). `WeakError` is just an alias for `Error | unknown`, because JavaScript lets you `throw` anything.
- `{ tag: 'expected', value: E }` a **typed domain error** you returned explicitly (only when using the [typed errors](typed-errors) feature).
- `{ tag: 'unexpected', value: WeakError }`: an **unexpected error** (network failure, thrown exception, etc.). `WeakError` is just an alias for `Error | unknown`, because JavaScript lets you `throw` anything.
- `{ tag: 'expected', value: E }`: a **typed domain error** you returned explicitly (only when using the [typed errors](typed-errors) feature).

If you're not using typed errors, every error will be `unexpected` so you can treat `failure.value` as the thrown value. Most of the time you won't inspect this at all; the `retry` callback is enough.
If you're not using typed errors, every error will be `unexpected`, so you can treat `failure.value` as the thrown value. Most of the time you won't inspect this at all; the `retry` callback is enough.

## Mutations fail the same way

Expand All @@ -99,4 +99,4 @@ const saveStore = useRemoteUpdate((name: string) => api.save(name));
saveStore.run('Alice');
```

You can also call `reset()` to go back to `Initial` instead of retrying useful when you'd rather let the user fix their input and try again.
You can also call `reset()` to go back to `Initial` instead of retrying. This is useful when you'd rather let the user fix their input and try again.
Loading
Loading