Skip to content

A React Context-based error UI rendering library. The Provider manages error state and displays error UI using user-provided custom renderers.

Installation

bash
pnpm add @sanghyuk-2i/huh-core @sanghyuk-2i/huh-react
bash
npm install @sanghyuk-2i/huh-core @sanghyuk-2i/huh-react
bash
yarn add @sanghyuk-2i/huh-core @sanghyuk-2i/huh-react

Peer Dependencies: react >= 18, react-dom >= 18


HuhProvider

Manages error state and invokes the appropriate type's renderer when an active error exists.

Props

ts
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

tsx
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:

tsx
// 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.

ts
type RendererMap = Record<string, (props: ErrorRenderProps) => ReactNode>;

In addition to built-in types (TOAST, MODAL, PAGE), renderers for custom types can be freely added:

ts
  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.

ts
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 message
  • error.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:

actionTypeBehavior
REDIRECTrouter.push(target) if router provided, otherwise window.location.href = target
BACKrouter.back() if router provided, otherwise window.history.back()
RETRYClear error + invoke onRetry callback
DISMISSClear error
Custom typeClear error + invoke onCustomAction callback
No actionClear error

Custom action types (e.g., OPEN_CHAT, SHARE) pass a { type, target } object to the onCustomAction callback.

Renderer Implementation Example

tsx
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.

ts

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:

  1. Check errorMap for code mapping
  2. Check if code directly matches a trackId
  3. Use fallbackTrackId
  4. Throw error if no mapping found
tsx
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:

tsx
<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.

tsx
import { clearError } = useHuh();

clearError();

Full Example

tsx
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>
  );
}

Released under the MIT License.