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 loader
const { 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 data
const { data, refetch, state } = useLoaderState(loader)
// ^? { users: User[], total: number }
// Without a loader, data is not available
const { refetch, state } = useLoaderState()
// ^? { refetch: () => void, state: 'idle' | 'loading' }
// Note: no 'data' property when called without loader
Migrating from useLoader to useLoaderState is straightforward:
// Before
import { useLoader } from 'one'
export default function Page() {
const data = useLoader(loader)
return <div>{data.content}</div>
}
// After
import { 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 client, 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.
Edit this page on GitHub.