hono-preact
Overview
Quick Start
The Route Table
Layouts & Nesting
Adding Pages
Head Management
Active Links
Signals
Server Loaders
Loading States
Reloading Data
Prefetching
Streaming
Live Loaders
Realtime Channels
Server Actions
Validation
Optimistic UI
Server Caller
View Transitions
Middleware
Session Channels
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
Listbox
Toast
renderElement
useControllableState
mergeRefs
useListNavigation
useTypeahead
useListboxSelection
usePosition

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:

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: { refetchActive: true } })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: { clear: [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 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();
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#