hono-preact
Overview
Quick Start
The Route Table
Layouts & Nesting
Adding Pages
Head Management
Active Links
Server Loaders
Loading States
Reloading Data
Prefetching
Streaming
Live Loaders
Realtime Channels
Server Actions
Validation
Optimistic UI
Server Caller
View Transitions
Middleware
CSRF Protection
CLI
Vite Config
Project Structure
Styling
Composing Hono Middleware
WebSockets
Rooms & Presence
renderPage
Link Prefetch
Build & Deploy
Overview
Dialog
Popover
Tooltip
Menu
Context Menu
Select
Combobox
Toast
renderElement
useControllableState
mergeRefs
useListNavigation
useTypeahead
useListboxSelection
usePosition
usePositioner
useDismiss
useFocusReturn

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.

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:

KnobTriggers fetch now?Clears cache?Affects what?
useReload().reload()YesYes (writes fresh data on success)The active page's loader (the one whose boundary you're inside).
loader.invalidate()NoYes (drops the entry)A specific loader's cache only. Next navigation that mounts the loader will refetch on cache miss.
useAction({ invalidate: 'auto' })YesYesAfter 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: [refA, refB] })SometimesYesAfter 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 'auto' mode is sugar over the reload path; its array mode is sugar over loader.invalidate() calls plus an opportunistic reload if the active loader is in the list.

A common surprise: invalidate: 'auto' is NOT a no-op even when the loader has no observable changes; it triggers a real network request through /__loaders. Use invalidate: false (the default) if you don't want a refetch after the action.

API#

const { reload, reloading } = useReload();
ValueTypeDescription
reload() => voidRe-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.
reloadingbooleantrue while an explicit reload or revalidation is in flight; false during the cold initial load and when idle.

See also#