A hook that provides manual refetch control and loading state for loaders. It extends the functionality of useLoader by adding the ability to manually refetch data and track loading states.
useLoaderState can be used in two different ways:
When you pass a loader function, you get back the data along with refetch and state:
import { useLoaderState } from 'one'
export function loader({ path }) { const url = new URL(path, 'http://localhost') const query = url.searchParams.get('q') || ''
return fetch(`/api/search?q=${query}`).then(r => r.json())}
export default function SearchPage() { const { data, refetch, state } = useLoaderState(loader)
return ( <div> <h1>Search Results</h1> {state === 'loading' ? ( <Spinner /> ) : ( <Results data={data} /> )} <button onClick={refetch} disabled={state === 'loading'}> Refresh Results </button> </div> )}When called without arguments, you can access the refetch function and state from any component in the tree:
import { useLoaderState } from 'one'
function RefreshButton() { const { refetch, state } = useLoaderState()
return ( <button onClick={refetch} disabled={state === 'loading'} > {state === 'loading' ? 'Refreshing...' : 'Refresh Page'} </button> )}
// This button can be placed anywhere in your component tree// and will refetch the current route's loaderconst { data, refetch, state } = useLoaderState(loader)data: The data returned by the loader (same as useLoader)refetch: A function to manually trigger a fresh loader fetchstate: Current loading state - either 'idle' or 'loading'const { refetch, state } = useLoaderState()refetch: A function to manually trigger a fresh loader fetchstate: Current loading state - either 'idle' or 'loading'All useLoaderState hooks on the same route share state through an internal subscription system. This means:
refetch() is called from any component, the loader cache for that route is clearedThis architecture enables powerful patterns like having a refresh button in your header that can refetch data for the current page, without needing to pass props down through your component tree.
function PullToRefreshWrapper({ children }) { const { refetch, state } = useLoaderState()
return ( <PullToRefresh onRefresh={refetch} refreshing={state === 'loading'}> {children} </PullToRefresh> )}function useLivePolling(intervalMs = 5000) { const { refetch, state } = useLoaderState()
useEffect(() => { const interval = setInterval(() => { // Only refetch if not already loading if (state === 'idle') { refetch() } }, intervalMs)
return () => clearInterval(interval) }, [refetch, state, intervalMs])}
export default function LiveDashboard() { const { data } = useLoaderState(loader) useLivePolling(3000) // Poll every 3 seconds
return <Dashboard data={data} />}export default function DataPage() { const { data, refetch, state } = useLoaderState(loader)
if (data?.error) { return ( <ErrorBoundary error={data.error} onRetry={refetch} retrying={state === 'loading'} /> ) }
return <PageContent data={data} />}function CommentForm({ postId }) { const { refetch } = useLoaderState()
const handleSubmit = async (formData) => { await submitComment(postId, formData) // Refetch the loader to get updated comments refetch() }
return ( <form onSubmit={handleSubmit}> {/* form fields */} </form> )}| Feature | useLoader | useLoaderState |
|---|---|---|
| Returns loader data | ✅ | ✅ |
| Manual refetch | ❌ | ✅ |
| Loading state | ❌ | ✅ |
| Can be called without loader | ❌ | ✅ |
| Can be used in child components | ❌ | ✅ |
Use useLoader when you only need the data and automatic refetching is sufficient.
Use useLoaderState when you need:
The hook is fully typed based on your loader’s return type:
export function loader() { return { users: [] as User[], total: 0 }}
// TypeScript knows the shape of dataconst { data, refetch, state } = useLoaderState(loader)// ^? { users: User[], total: number }
// Without a loader, data is not availableconst { refetch, state } = useLoaderState()// ^? { refetch: () => void, state: 'idle' | 'loading' }// Note: no 'data' property when called without loaderMigrating from useLoader to useLoaderState is straightforward:
// Beforeimport { useLoader } from 'one'
export default function Page() { const data = useLoader(loader) return <div>{data.content}</div>}
// Afterimport { useLoaderState } from 'one'
export default function Page() { const { data, refetch, state } = useLoaderState(loader) return ( <div> {state === 'loading' && <LoadingSpinner />} {data.content} <button onClick={refetch}>Refresh</button> </div> )}useLoaderState works consistently across all rendering modes:
+spa): Loaders execute on the server for each request, refetch triggers client-side fetching+ssr): Initial load on server, refetch executes on client+ssg): Build-time data as initial state, refetch provides fresh client dataThe refetch functionality always executes on the client, ensuring consistent behavior regardless of the initial rendering mode.
For debugging and performance monitoring, you can access the timing history of loader executions:
import { getLoaderTimingHistory } from 'one'
function DebugPanel() { const timings = getLoaderTimingHistory()
return ( <div> {timings.map((entry) => ( <div key={`${entry.path}:${entry.startTime}`}> <strong>{entry.path}</strong>: {entry.totalTime}ms {entry.error && <span> (Error: {entry.error})</span>} </div> ))} </div> )}The LoaderTimingEntry type contains:
type LoaderTimingEntry = { path: string // Route path (e.g., '/users/123') startTime: number // Timestamp when loader started moduleLoadTime?: number // Time to load the loader module executionTime?: number // Time to execute the loader totalTime?: number // Total time from start to finish error?: string // Error message if loader failed source: 'preload' | 'initial' | 'refetch' // What triggered the load}This function is useful for:
The history is only populated in development mode and stores the last 50 entries to avoid memory growth.
Edit this page on GitHub.