A React Context-based error UI rendering library. The Provider manages error state and displays error UI using user-provided custom renderers.
Installation
pnpm add @sanghyuk-2i/huh-core @sanghyuk-2i/huh-reactnpm install @sanghyuk-2i/huh-core @sanghyuk-2i/huh-reactyarn add @sanghyuk-2i/huh-core @sanghyuk-2i/huh-reactPeer Dependencies: react >= 18, react-dom >= 18
HuhProvider
Manages error state and invokes the appropriate type's renderer when an active error exists.
Props
interface HuhProviderProps {
source?: ErrorConfig; // JSON DSL data (single language mode)
locales?: LocalizedErrorConfig; // Multi-language error config (i18n mode)
defaultLocale?: string; // Default locale
locale?: string; // Current locale (externally controlled)
renderers: RendererMap; // Custom renderers (required)
children: ReactNode;
onRetry?: () => void; // Callback invoked on RETRY action
onCustomAction?: (action: { type: string; target?: string }) => void; // Custom action callback
plugins?: HuhPlugin[]; // Plugin array for monitoring/analytics
errorMap?: Record<string, string>; // Error code to trackId mapping table
fallbackTrackId?: string; // Default trackId when no mapping is found
router?: HuhRouter; // Custom router for client-side navigation (e.g., Next.js useRouter)
}TIP
Either source or locales must be provided. source is for single language mode, locales is for multi-language mode.
Basic Usage
import errorContent from './huh.json';
import { HuhProvider } from '@sanghyuk-2i/huh-react';
function App() {
return (
<HuhProvider
source={errorContent}
renderers={renderers}
onRetry={() => window.location.reload()}
onCustomAction={(action) => {
// Handle custom action types (e.g., OPEN_CHAT, SHARE, etc.)
if (action.type === 'OPEN_CHAT') openChatWidget();
}}
>
<YourApp />
</HuhProvider>
);
}Router Integration
Pass a router prop to use framework-specific client-side navigation instead of full page reloads:
// Next.js
import { useRouter } from 'next/navigation';
function App() {
const router = useRouter();
return (
<HuhProvider
source={errorContent}
renderers={renderers}
router={{ push: router.push, back: router.back }}
>
<YourApp />
</HuhProvider>
);
}When router is provided, REDIRECT actions use router.push() and BACK actions use router.back(). Without it, the default window.location.href and window.history.back() behavior is preserved.
RendererMap
Provides renderers for each error type. There are no default renderers -- if an error occurs and no renderer exists for that type, a runtime error will be thrown. Keys are uppercase type names.
type RendererMap = Record<string, (props: ErrorRenderProps) => ReactNode>;In addition to built-in types (TOAST, MODAL, PAGE), renderers for custom types can be freely added:
TOAST: ({ error, onDismiss }) => <Toast message={error.message} onClose={onDismiss} />,
MODAL: ({ error, onAction, onDismiss }) => <Modal ... />,
PAGE: ({ error, onAction }) => <ErrorPage ... />,
// Custom type renderers
BANNER: ({ error, onAction, onDismiss }) => <Banner message={error.message} ... />,
SNACKBAR: ({ error, onDismiss }) => <Snackbar message={error.message} ... />,
};ErrorRenderProps
Props passed to each renderer.
interface ErrorRenderProps {
error: ResolvedError; // Error info with variables already substituted
onAction: () => void; // Action button click handler
onDismiss: () => void; // Dismiss handler
}error.type--'TOAST' | 'MODAL' | 'PAGE' | string(includes custom types)error.message-- Substituted messageerror.title-- Substituted title (optional)error.image-- Image URL (optional)error.action-- Action info (optional)
onAction Behavior
onAction behaves automatically based on the error's action.type:
| actionType | Behavior |
|---|---|
REDIRECT | router.push(target) if router provided, otherwise window.location.href = target |
BACK | router.back() if router provided, otherwise window.history.back() |
RETRY | Clear error + invoke onRetry callback |
DISMISS | Clear error |
| Custom type | Clear error + invoke onCustomAction callback |
| No action | Clear error |
Custom action types (e.g., OPEN_CHAT, SHARE) pass a { type, target } object to the onCustomAction callback.
Renderer Implementation Example
import type { RendererMap } from '@sanghyuk-2i/huh-react';
import { Toast } from '@/components/Toast';
import { Modal } from '@/components/Modal';
const renderers: RendererMap = {
TOAST: ({ error, onDismiss }) => <Toast message={error.message} onClose={onDismiss} />,
MODAL: ({ error, onAction, onDismiss }) => (
<Modal open onClose={onDismiss}>
<Modal.Title>{error.title}</Modal.Title>
<Modal.Body>{error.message}</Modal.Body>
<Modal.Footer>
{error.action && <button onClick={onAction}>{error.action.label}</button>}
<button onClick={onDismiss}>Close</button>
</Modal.Footer>
</Modal>
),
PAGE: ({ error, onAction }) => (
<div className="flex flex-col items-center justify-center min-h-screen">
{error.image && <img src={error.image} alt="" className="w-48 mb-8" />}
<h1 className="text-3xl font-bold">{error.title}</h1>
<p className="mt-4 text-gray-600">{error.message}</p>
{error.action && (
<button onClick={onAction} className="mt-8 btn btn-primary">
{error.action.label}
</button>
)}
</div>
),
// Custom type example
BANNER: ({ error, onAction, onDismiss }) => (
<div className="bg-yellow-100 border-l-4 border-yellow-500 p-4">
<p>{error.message}</p>
{error.action && <button onClick={onAction}>{error.action.label}</button>}
<button onClick={onDismiss}>Close</button>
</div>
),
};useHuh
A hook for triggering or clearing errors from within the Provider tree.
interface HuhContextValue {
huh: (code: string, variables?: Record<string, string>) => void;
clearError: () => void;
locale: string | undefined; // Current locale (i18n mode)
setLocale: (locale: string) => void; // Change locale (i18n mode)
}TIP
Throws an error if called outside of the Provider.
huh(code, variables?)
The single function for triggering errors. Handles direct trackId, errorMap mapping, and fallback.
Lookup order:
- Check
errorMapfor code mapping - Check if code directly matches a trackId
- Use
fallbackTrackId - Throw error if no mapping found
import { huh } = useHuh();
// Trigger by trackId directly
huh('ERR_NETWORK');
// Trigger with variable substitution
huh('ERR_SESSION_EXPIRED', { userName: 'Jane' });
// Map API error codes via errorMap
try {
await api.call();
} catch (e) {
huh(e.code); // 'API_500' → errorMap → 'ERR_SERVER'
}errorMap setup:
<HuhProvider
source={config}
renderers={renderers}
errorMap={{ API_500: 'ERR_SERVER', API_401: 'ERR_AUTH' }}
fallbackTrackId="ERR_UNKNOWN"
>
<App />
</HuhProvider>clearError()
Closes the currently active error UI.
import { clearError } = useHuh();
clearError();Full Example
import React from 'react';
import errorContent from './huh.json';
import { HuhProvider, useHuh } from '@sanghyuk-2i/huh-react';
import type { RendererMap } from '@sanghyuk-2i/huh-react';
const renderers: RendererMap = {
TOAST: ({ error, onDismiss }) => (
<div className="fixed bottom-4 right-4 bg-red-500 text-white p-4 rounded">
{error.message}
<button onClick={onDismiss} className="ml-2">
X
</button>
</div>
),
MODAL: ({ error, onAction, onDismiss }) => (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<div className="bg-white p-6 rounded-lg">
<h2 className="text-xl font-bold">{error.title}</h2>
<p className="mt-2">{error.message}</p>
<div className="mt-4 flex gap-2">
{error.action && (
<button onClick={onAction} className="btn-primary">
{error.action.label}
</button>
)}
<button onClick={onDismiss}>Close</button>
</div>
</div>
</div>
),
PAGE: ({ error, onAction }) => (
<div className="min-h-screen flex flex-col items-center justify-center">
<h1 className="text-4xl">{error.title}</h1>
<p className="mt-4">{error.message}</p>
{error.action && (
<button onClick={onAction} className="mt-8 btn-primary">
{error.action.label}
</button>
)}
</div>
),
};
function UserProfile() {
const { huh } = useHuh();
const loadProfile = async () => {
try {
const res = await fetch('/api/profile');
if (!res.ok) throw new Error();
} catch {
huh('ERR_PROFILE_LOAD');
}
};
return <button onClick={loadProfile}>Load Profile</button>;
}
export default function App() {
return (
<HuhProvider
source={errorContent}
renderers={renderers}
onRetry={() => console.log('Retrying...')}
>
<UserProfile />
</HuhProvider>
);
}