Hooks
React hooks for accessing route data and navigation state.
Import
import {
useLoaderData,
useRouteLoaderData,
useActionData,
useNavigation,
useError,
useRouteError,
isRouteErrorResponse,
useParams,
useSearchParams,
useLocation,
} from '@ereo/client'
// Types
import type {
NavigationStatus,
NavigationStateHook,
LocationState,
LoaderDataContextValue,
ActionDataContextValue,
NavigationContextValue,
ErrorContextValue,
ParamsContextValue,
LocationContextValue,
LoaderDataProviderProps,
ActionDataProviderProps,
NavigationProviderProps,
ErrorProviderProps,
ParamsProviderProps,
LocationProviderProps,
EreoProviderProps,
} from '@ereo/client'Types
// Navigation status for useNavigation hook
type NavigationStatus = 'idle' | 'loading' | 'submitting'
// Navigation state returned by useNavigation
interface NavigationStateHook {
status: NavigationStatus
location?: string // The URL being navigated to
formData?: FormData // Form data being submitted
formMethod?: string // Form method being used
formAction?: string // Form action URL
}
// Location object returned by useLocation
interface LocationState {
pathname: string // e.g., '/users/123'
search: string // e.g., '?page=1&sort=name'
hash: string // e.g., '#section-2'
state: unknown // History state object
key: string // Unique key for this location entry
}useLoaderData
Accesses the data returned by the route's loader.
Signature
function useLoaderData<T = unknown>(): TExample
import { useLoaderData } from '@ereo/client'
interface LoaderData {
posts: Post[]
totalCount: number
}
export default function Posts() {
const { posts, totalCount } = useLoaderData<LoaderData>()
return (
<div>
<h1>Posts ({totalCount})</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}With Type Inference
import { createLoader } from '@ereo/data'
import { useLoaderData } from '@ereo/client'
export const loader = createLoader(async () => {
const posts = await db.posts.findMany()
return { posts, count: posts.length }
})
export default function Posts() {
// Type is inferred from loader
const { posts, count } = useLoaderData<Awaited<ReturnType<typeof loader>>>()
return <div>...</div>
}useRouteLoaderData
Accesses loader data from a specific route by its ID. Useful for reading data from a parent layout or sibling route without prop drilling.
Signature
function useRouteLoaderData<T = unknown>(routeId: string): T | undefinedReturns undefined if no matching route is found.
Example
import { useRouteLoaderData } from '@ereo/client'
// Access root layout's loader data from any nested route
function UserAvatar() {
const rootData = useRouteLoaderData<{ user: User }>('root-layout')
if (!rootData?.user) return null
return <img src={rootData.user.avatar} alt={rootData.user.name} />
}useActionData
Accesses the data returned by the route's action.
Signature
function useActionData<T = unknown>(): T | undefinedReturns undefined if no action has been submitted.
Example
import { useActionData } from '@ereo/client'
import { Form } from '@ereo/client'
interface ActionData {
error?: string
success?: boolean
values?: { email: string }
}
export default function Subscribe() {
const actionData = useActionData<ActionData>()
return (
<Form method="post">
<input
name="email"
type="email"
defaultValue={actionData?.values?.email}
/>
{actionData?.error && (
<p className="error">{actionData.error}</p>
)}
{actionData?.success && (
<p className="success">Subscribed successfully!</p>
)}
<button type="submit">Subscribe</button>
</Form>
)
}Handling Multiple States
export default function ContactForm() {
const actionData = useActionData<{
errors?: Record<string, string>
success?: boolean
formData?: Record<string, string>
}>()
return (
<Form method="post">
<div>
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
defaultValue={actionData?.formData?.name}
/>
{actionData?.errors?.name && (
<span className="error">{actionData.errors.name}</span>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
defaultValue={actionData?.formData?.email}
/>
{actionData?.errors?.email && (
<span className="error">{actionData.errors.email}</span>
)}
</div>
<button type="submit">Send</button>
</Form>
)
}useNavigation
Tracks the current navigation state.
Signature
function useNavigation(): NavigationStateHook
interface NavigationStateHook {
status: 'idle' | 'loading' | 'submitting'
location?: string // The URL being navigated to
formData?: FormData
formMethod?: string
formAction?: string
}States
idle- No navigation in progressloading- Navigating to a new page (loader running)submitting- Form submission in progress (action running)
Example
import { useNavigation } from '@ereo/client'
export default function Page() {
const navigation = useNavigation()
return (
<div>
{navigation.status === 'loading' && (
<div className="loading-bar" />
)}
<h1>Page Content</h1>
</div>
)
}Form Submit Indicator
import { useNavigation } from '@ereo/client'
import { Form } from '@ereo/client'
export default function NewPost() {
const navigation = useNavigation()
const isSubmitting = navigation.status === 'submitting'
return (
<Form method="post">
<input name="title" disabled={isSubmitting} />
<textarea name="content" disabled={isSubmitting} />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create Post'}
</button>
</Form>
)
}Global Loading Indicator
// components/LoadingIndicator.tsx
import { useNavigation } from '@ereo/client'
export function LoadingIndicator() {
const navigation = useNavigation()
if (navigation.status === 'idle') {
return null
}
return (
<div className="fixed top-0 left-0 right-0 h-1 bg-blue-500 animate-pulse" />
)
}Optimistic UI
import { useNavigation } from '@ereo/client'
import { Form } from '@ereo/client'
export default function TodoItem({ todo }) {
const navigation = useNavigation()
// Check if this item is being toggled
const isToggling =
navigation.status === 'submitting' &&
navigation.formData?.get('todoId') === todo.id &&
navigation.formData?.get('intent') === 'toggle'
// Optimistic completed state
const completed = isToggling ? !todo.completed : todo.completed
return (
<Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<input type="hidden" name="intent" value="toggle" />
<button type="submit" className={completed ? 'completed' : ''}>
{todo.title}
</button>
</Form>
)
}useError
Accesses the error thrown in an error boundary.
Signature
function useError(): Error | undefinedExample
import { useError } from '@ereo/client'
export function ErrorBoundary() {
const error = useError()
return (
<div className="error-page">
<h1>Oops!</h1>
<p>{error?.message || 'Something went wrong'}</p>
<a href="/">Go home</a>
</div>
)
}useRouteError
Accesses the error thrown in a route's error boundary. Unlike useError (which returns Error | undefined), useRouteError returns unknown — this allows you to distinguish HTTP error responses from runtime errors using isRouteErrorResponse.
Signature
function useRouteError(): unknownExample
import { useRouteError, isRouteErrorResponse } from '@ereo/client'
export default function PostsError() {
const error = useRouteError()
if (isRouteErrorResponse(error)) {
// HTTP error response (e.g., thrown from a loader)
if (error.status === 404) {
return <h1>Post not found</h1>
}
return <h1>Error {error.status}: {error.statusText}</h1>
}
// Runtime error
return <h1>Something went wrong</h1>
}
useErrorvsuseRouteError: UseuseErrorwhen you just need the error message. UseuseRouteErrorwhen you need to check if the error is an HTTP error response (e.g., 404, 403) and render different UI based on the status code.
isRouteErrorResponse
Type guard that checks if an error is an HTTP error response (has status, statusText, and data properties). Use with useRouteError to distinguish HTTP errors from runtime errors.
Signature
function isRouteErrorResponse(error: unknown): error is RouteErrorResponse
interface RouteErrorResponse {
status: number
statusText: string
data?: unknown
}Example
import { useRouteError, isRouteErrorResponse } from '@ereo/client'
export default function ErrorPage() {
const error = useRouteError()
if (isRouteErrorResponse(error)) {
switch (error.status) {
case 404:
return <h1>Page not found</h1>
case 401:
return <h1>Unauthorized — please log in</h1>
case 403:
return <h1>You don't have permission to view this page</h1>
default:
return <h1>Error {error.status}</h1>
}
}
return (
<div>
<h1>Something went wrong</h1>
<p>{error instanceof Error ? error.message : 'Unknown error'}</p>
</div>
)
}useParams
Accesses the URL parameters from dynamic route segments.
Signature
function useParams<T extends Record<string, string> = Record<string, string>>(): TExample
import { useParams } from '@ereo/client'
// In a route file at routes/users/[id].tsx
function UserProfile() {
const { id } = useParams<{ id: string }>()
return <div>User ID: {id}</div>
}useSearchParams
Accesses and modifies the current URL search parameters. Returns a tuple of [URLSearchParams, setSearchParams], similar to useState.
Signature
function useSearchParams(): [
URLSearchParams,
(
nextParams:
| URLSearchParams
| Record<string, string>
| ((prev: URLSearchParams) => URLSearchParams | Record<string, string>),
options?: { replace?: boolean }
) => void
]Example
import { useSearchParams } from '@ereo/client'
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams()
const page = searchParams.get('page') || '1'
const sort = searchParams.get('sort') || 'name'
return (
<div>
<select
value={sort}
onChange={(e) => setSearchParams({ page, sort: e.target.value })}
>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
<button onClick={() => setSearchParams({ page: String(Number(page) + 1), sort })}>
Next Page
</button>
{/* Use replace to avoid adding to history */}
<button onClick={() => setSearchParams({ page: '1', sort }, { replace: true })}>
Reset
</button>
</div>
)
}Functional Updates
// Update based on previous params
setSearchParams((prev) => {
const next = new URLSearchParams(prev)
next.set('page', '2')
return next
})useLocation
Accesses the current location object.
Signature
function useLocation(): LocationState
interface LocationState {
pathname: string // e.g., '/users/123'
search: string // e.g., '?page=1'
hash: string // e.g., '#section-2'
state: unknown // History state object
key: string // Unique key for this location entry
}Example
import { useLocation } from '@ereo/client'
function Breadcrumbs() {
const location = useLocation()
const segments = location.pathname.split('/').filter(Boolean)
return (
<nav>
<a href="/">Home</a>
{segments.map((seg, i) => (
<span key={i}>
{' / '}
<a href={'/' + segments.slice(0, i + 1).join('/')}>{seg}</a>
</span>
))}
</nav>
)
}Context Providers
EreoJS exports context providers for testing, custom setups, and direct context access.
Import
import {
// Providers
LoaderDataProvider,
ActionDataProvider,
NavigationProvider,
ErrorProvider,
ParamsProvider,
LocationProvider,
EreoProvider,
// Contexts (for useContext)
LoaderDataContext,
ActionDataContext,
NavigationContext,
ErrorContext,
ParamsContext,
LocationContext,
// Internal context accessors
useLoaderDataContext,
useActionDataContext,
useNavigationContext,
useErrorContext,
} from '@ereo/client'Provider Props
interface LoaderDataProviderProps {
children: ReactNode
initialData?: unknown
}
interface ActionDataProviderProps {
children: ReactNode
initialData?: unknown
}
interface NavigationProviderProps {
children: ReactNode
initialState?: NavigationStateHook
}
interface ErrorProviderProps {
children: ReactNode
initialError?: Error
}
interface ParamsProviderProps {
children: ReactNode
initialParams?: Record<string, string>
}
interface LocationProviderProps {
children: ReactNode
initialLocation?: LocationState
}
interface EreoProviderProps {
children: ReactNode
loaderData?: unknown // Initial loader data (typically from SSR)
actionData?: unknown // Initial action data
navigationState?: NavigationStateHook // Initial navigation state
error?: Error // Initial error (if rendering error boundary)
params?: Record<string, string> // Initial route params (from route matching)
location?: LocationState // Initial location (from request URL or window.location)
matches?: RouteMatchData[] // Initial matched routes (from route matching)
}EreoProvider
Combines all providers into a single wrapper:
import { EreoProvider } from '@ereo/client'
function App({ loaderData, actionData }) {
return (
<EreoProvider loaderData={loaderData} actionData={actionData}>
<Routes />
</EreoProvider>
)
}Individual Providers
import { LoaderDataProvider, ActionDataProvider } from '@ereo/client'
function App() {
return (
<LoaderDataProvider initialData={{ posts: [] }}>
<ActionDataProvider>
<MyComponent />
</ActionDataProvider>
</LoaderDataProvider>
)
}Testing with Providers
import { render } from '@testing-library/react'
import { LoaderDataProvider } from '@ereo/client'
import PostList from './PostList'
test('renders posts', () => {
render(
<LoaderDataProvider initialData={{ posts: [{ id: 1, title: 'Test Post' }] }}>
<PostList />
</LoaderDataProvider>
)
expect(screen.getByText('Test Post')).toBeInTheDocument()
})Context Accessors (Internal)
These hooks provide direct access to the full context value including setters:
// Get full loader data context with setter
const { data, setData } = useLoaderDataContext()
// Get full action data context with setter and clear
const { data, setData, clearData } = useActionDataContext()
// Get full navigation context with state controls
const { state, setState, startLoading, startSubmitting, complete } = useNavigationContext()
// Get full error context with setter and clear
const { error, setError, clearError } = useErrorContext()Custom Hooks
Build on top of the base hooks:
useIsSubmitting
function useIsSubmitting(action?: string) {
const navigation = useNavigation()
if (action) {
return (
navigation.status === 'submitting' &&
navigation.formAction === action
)
}
return navigation.status === 'submitting'
}useIsLoading
function useIsLoading() {
const navigation = useNavigation()
return navigation.status === 'loading'
}useFormStatus
function useFormStatus() {
const navigation = useNavigation()
const actionData = useActionData()
return {
isSubmitting: navigation.status === 'submitting',
isSuccess: actionData?.success === true,
isError: actionData?.error !== undefined,
error: actionData?.error
}
}