Reloading Data#
Sometimes you need to re-run the loader imperatively, for example after a user adds a record to a table. The useReload hook lets you do this from within a page component.
Basic usage#
Snippets on this page assume type MovieList = { results: { id: number; title: string }[] }.
src/pages/movies.server.ts holds the loader (the cache is auto-attached):
import { defineLoader } from 'hono-preact';
export const serverLoaders = {
default: defineLoader(async () => ({
movies: await getMovies(),
})),
};
src/pages/movies.tsx uses .View() to create the component. useReload is called inside the render function, which runs inside the loader's boundary:
import { definePage, useReload } from 'hono-preact';
import { serverLoaders } from './movies.server.js';
const dataLoader = serverLoaders.default;
const MoviesView = dataLoader.View(({ data }) => {
const { reload, reloading } = useReload();
const handleAdd = async () => {
await addMovie({ title: 'New Movie' });
reload();
};
if (!data) return <p>Loading...</p>;
return (
<>
<button onClick={handleAdd} disabled={reloading}>
{reloading ? 'Adding...' : 'Add Movie'}
</button>
<ul>
{data.movies.results.map((m) => (
<li key={m.id}>{m.title}</li>
))}
</ul>
</>
);
});
export default definePage(MoviesView);
src/routes.ts wires the URL to the view; the colocated movies.server.ts is auto-discovered with it:
{
path: '/movies',
view: () => import('./pages/movies.js'),
}
useReload must be called inside a component rendered within a loader boundary (inside .View() or inside a loader.Boundary). Calling it outside throws an error.
Background refresh#
Reload is a background refresh. While reloading is true, the loader's previous value is retained so the current content stays visible; reloading reflects an explicit reload or revalidation only, never the cold initial load. Inside a .View() render function the same in-flight refresh surfaces as the revalidating status, whose arm still carries data. Use reloading (from useReload) or the revalidating status (from the .View() render arg) to reflect the in-progress state in your UI. reloading is true only while revalidating a value that already exists; a reload or retry of a loader that has not produced a value yet (a failed first load, or a live reconnect after a cold pre-first-chunk error) is itself a cold load, so branch on status === 'loading' for that case, not reloading.
Live loaders read under .Boundary + useData(initial, reduce) report the reconnect on their own status instead. While a resubscribe is in flight over chunks already delivered, status is 'reconnecting' and the arm still carries data, so the last good fold stays on screen. reloading stays false there on purpose: it lives on the loader host, and making it follow a live stream would re-render that host on stream activity, which is exactly what collect-mode avoids. Branch on status === 'reconnecting' and only the component that reads it updates.
Three knobs, three behaviors#
The framework has three ways to invalidate or re-run a loader. They look similar at the call site but mean different things at runtime:
| Knob | Triggers fetch now? | Clears cache? | Affects what? |
|---|---|---|---|
useReload().reload() | Yes | Yes (writes fresh data on success) | The active page's loader (the one whose boundary you're inside). |
loader.invalidate() | No | Yes (drops the entry) | A specific loader's cache only. Next navigation that mounts the loader will refetch on cache miss. |
useAction({ invalidate: { refetchActive: true } }) | Yes | Yes | After the action succeeds, re-runs the active page's loader (the one wrapping the useAction call). Equivalent to calling useReload().reload() inside onSuccess. |
useAction({ invalidate: { clear: [refA, refB] } }) | Sometimes | Yes | After the action succeeds, calls .invalidate() on each ref. If any ref is the active page's loader, ALSO re-runs that loader; sibling-page loaders just have their cache cleared and refetch on their next mount. |
The mental model: invalidate is "mark stale, refetch lazily". reload is "fetch right now". useAction's refetchActive field is sugar over the reload path; its clear field is sugar over loader.invalidate() calls, and refetchActive defaults to true when the active loader is in clear.
A common surprise: invalidate: { refetchActive: true } is NOT a no-op even when the loader has no observable changes; it triggers a real network request through /__loaders. Omit invalidate (the default) if you don't want a refetch after the action.
API#
const { reload, reloading } = useReload();
| Value | Type | Description |
|---|---|---|
reload | () => void | Re-runs the serverLoader. If called while a fetch (initial load or a previous reload) is still in flight, the call is queued and runs once the in-flight fetch settles; concurrent calls coalesce into a single queued run. |
reloading | boolean | true while an explicit reload or revalidation is in flight; false during the cold initial load and when idle. |
See also#
- Server Loaders:
loader.invalidate()in context. - Server Actions:
useActioninvalidate option. - Loading States: switching on the
LoaderStateunion during a refetch.