Mutations
Most applications don't just read data — they also write it.
use-remote-data now has first-class support for mutations through useRemoteUpdate,
giving you the same principled, type-safe experience for writes that useRemoteData provides for reads.
The basics
The useRemoteUpdate hook takes a function that performs a write operation and returns a RemoteUpdateStore.
Unlike useRemoteData, nothing happens until you call run() — the mutation is entirely imperative.
import { useState } from 'react';
import { useRemoteUpdate, AwaitUpdate } from 'use-remote-data';
const saveItem = (name: string): Promise<string> =>
new Promise((resolve) =>
setTimeout(
() =>
resolve(
`saved "${name}" with id #${Math.floor(Math.random() * 1000)}`
),
800
)
);
export function Component() {
const [name, setName] = useState('');
const store = useRemoteUpdate((n: string) => saveItem(n), {
storeName: 'Save item',
});
return (
<div>
<label>
name:{' '}
<input
value={name}
onChange={(e) => setName(e.currentTarget.value)}
/>
</label>{' '}
<button
onClick={() => store.run(name)}
disabled={!name || store.current.type === 'pending'}
>
Save
</button>
<AwaitUpdate store={store}>{(msg) => <p>✓ {msg}</p>}</AwaitUpdate>
</div>
);
}
The store starts in an Initial state. When run() is called, it transitions to Pending, then to Success (success)
or Failed (failure). The entire RemoteData state machine you already know applies here too!
Parameters
The function you pass to useRemoteUpdate can accept parameters.
Pass them to run() at call-time — no dependencies array needed.
import { useState } from 'react';
import { useRemoteUpdate, AwaitUpdate } from 'use-remote-data';
type UserParams = { firstName: string; lastName: string };
const createUser = (params: UserParams): Promise<string> =>
new Promise((resolve) =>
setTimeout(
() => resolve(`Created ${params.firstName} ${params.lastName}`),
800
)
);
export function Component() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const store = useRemoteUpdate((p: UserParams) => createUser(p));
return (
<div>
<input
placeholder="First name"
value={firstName}
onChange={(e) => setFirstName(e.currentTarget.value)}
/>{' '}
<input
placeholder="Last name"
value={lastName}
onChange={(e) => setLastName(e.currentTarget.value)}
/>{' '}
<button
onClick={() => store.run({ firstName, lastName })}
disabled={
!firstName || !lastName || store.current.type === 'pending'
}
>
Create
</button>
<AwaitUpdate store={store}>
{(msg, run, reset) => (
<div>
<p>{msg}</p>
<button onClick={() => run({ firstName, lastName })}>
Create another
</button>{' '}
<button onClick={() => reset()}>Clear</button>
</div>
)}
</AwaitUpdate>
</div>
);
}
This is a key difference from useRemoteData:
form values are passed when the user clicks submit, not captured in a closure that re-creates on every keystroke.
Automatic refresh
After a successful mutation, you usually need to refresh some read stores.
The refreshes option does this for you — just pass the stores that should refresh:
import {
useRemoteData,
useRemoteUpdate,
Await,
AwaitUpdate,
} from 'use-remote-data';
let counter = 0;
const fetchCount = (): Promise<number> =>
new Promise((resolve) => setTimeout(() => resolve(counter), 500));
const increment = (): Promise<string> =>
new Promise((resolve) => {
counter++;
setTimeout(() => resolve(`incremented to ${counter}`), 500);
});
export function Component() {
const countStore = useRemoteData(fetchCount);
const incrementStore = useRemoteUpdate(() => increment(), {
refreshes: [countStore],
});
return (
<div>
<Await store={countStore}>
{(count, isStale) => (
<span style={{ color: isStale ? 'gray' : 'black' }}>
Count: {count}
</span>
)}
</Await>{' '}
<button
onClick={() => incrementStore.run()}
disabled={incrementStore.current.type === 'pending'}
>
Increment
</button>
<AwaitUpdate store={incrementStore}>
{(msg) => <p>✓ {msg}</p>}
</AwaitUpdate>
</div>
);
}
After success, each store in the array has its refresh() called automatically.
Any Await rendering those stores will re-fetch.
Success and error callbacks
For fire-and-forget patterns like dialogs, you can use onSuccess to perform side effects
like closing a dialog — without putting imperative code in your render functions:
const store = useRemoteUpdate((params) => saveItem(params), {
refreshes: [itemsStore],
onSuccess: () => dialog.close(),
onError: (errors) => toast.error('Save failed'),
});
The callback runs once, after the refresh completes.
Resetting
Call reset() to return the store to its Initial state.
This is perfect for dialogs — reset when closing so the next open starts fresh:
const handleClose = () => {
store.reset();
dialog.close();
};
You can also reset after an error (as an alternative to retrying with the same parameters), or after success to clear the result.
Rendering with AwaitUpdate
For cases where you want to render the mutation result, use AwaitUpdate.
It works like Await but never auto-triggers — fetching only starts when run() is called.
The children receive the data, the run function (for re-runs), and reset:
import { useRemoteUpdate, AwaitUpdate } from 'use-remote-data';
let attempts = 0;
const riskyOperation = (): Promise<string> =>
new Promise((resolve, reject) => {
attempts++;
setTimeout(() => {
if (attempts % 3 === 0)
reject(new Error('server error (every 3rd attempt fails)'));
else resolve(`success on attempt #${attempts}`);
}, 800);
});
export function Component() {
const store = useRemoteUpdate(() => riskyOperation());
return (
<div>
<AwaitUpdate
store={store}
idle={(run) => (
<button onClick={() => run()}>Start operation</button>
)}
>
{(result, run, reset) => (
<div>
<p>Result: {result}</p>
<button onClick={() => run()}>Run again</button>{' '}
<button onClick={() => reset()}>Reset</button>
</div>
)}
</AwaitUpdate>
</div>
);
}
The optional idle prop controls what renders before the mutation has ever been triggered.
Retries
If a mutation fails, the default error view automatically shows a retry button — just like read stores. Retry re-runs with the same parameters that were originally passed.