Skip to main content

Documentation Index

Fetch the complete documentation index at: https://handbook.mhchq.ai/llms.txt

Use this file to discover all available pages before exploring further.

Dashboard pages in OpenInsure store filter and view state in the URL via nuqs (useQueryState). This means every filter a user sets — search term, date range, status filter, line-of-business selector — survives navigation, page refresh, and can be shared as a link that opens to the exact filtered view.
Tip: nuqs is a small, zero-dependency library (< 2 KB) that maps React state to URL search params with no boilerplate. It is the only way to store filter state in OpenInsure portals — useState for filter values is a bug.

Why the URL is the right place for filter state

Raw useState for filters causes three concrete user-facing bugs:
  1. Navigate away → filters reset — The user builds a filtered view, clicks into a record, hits back, and lands on the unfiltered default. They have to rebuild their query.
  2. Refresh → filters reset — Same problem. Every refresh destroys work.
  3. Can’t share a view — “Send me that filtered list” is not possible.
URL params fix all three with zero extra effort from the user.

Setup — NuqsAdapter

Each Next.js app needs NuqsAdapter wrapped around its children in providers.tsx. This is a one-time setup per app.
app/providers.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider>
      <NuqsAdapter>{children}</NuqsAdapter>
    </QueryClientProvider>
  );
}
No configuration needed. The adapter handles serialization, history mode, and shallow routing for you.

Usage — useQueryState

import { parseAsString, useQueryState } from 'nuqs';

// String filter with a default value
const [status, setStatus] = useQueryState('status', parseAsString.withDefault('all'));

// Multi-value filter (array)
import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs';
const [statuses, setStatuses] = useQueryState(
  'statuses',
  parseAsArrayOf(parseAsString).withDefault([])
);
Drop-in replacement for useState. The component API is identical — [value, setValue] — and it works with all existing controlled inputs and Select components unchanged.

Wiring to TanStack Query re-fetches

Include the URL-bound value in the queryKey. When the filter changes, the URL updates, the component re-renders, and TanStack Query automatically re-fetches with the new key:
const [category, setCategory] = useQueryState('category', parseAsString.withDefault('all'));

const { data } = useQuery({
  queryKey: ['audit-events', category], // <-- nuqs value here
  queryFn: async () => {
    const params = new URLSearchParams();
    if (category !== 'all') params.set('resource_type', category);
    const res = await fetch(`/api/v1/audit?${params}`);
    return res.json();
  },
});
Client-side filters (search text, severity) that don’t need a re-fetch can still use useQueryState for URL persistence — they just aren’t included in queryKey.

URL conventions

Carrier Portal

PageParamValuesEffect
/audit?category=all, program, claim, policy, auth, bordereauxTriggers API re-fetch
/audit?severity=all, info, warning, error, criticalClient-side filter
/audit?q=any stringClient-side search
/claims/analytics?range=30d, 90d, 6m, 12m, ytdControls chart period
/claims/analytics?lob=all, auto, gl, wc, propTriggers API re-fetch
/financials?range=30d, 90d, 6m, 12m, ytdControls chart period

UW Workbench

PageParamValuesEffect
/triage?priorities=comma-separated: high, medium, lowClient-side filter
/triage?statuses=comma-separated status valuesClient-side filter
/triage?assignedTo=user ID or ''Client-side filter
/submissions?q=any stringClient-side search
/submissions?statuses=comma-separated status valuesTriggers re-fetch
/policies?q=any stringClient-side search
/policies?statuses=comma-separated status valuesTriggers re-fetch

Primitives reference

PrimitiveImportUse case
parseAsStringnuqsSingle-value filters (status, category, search)
parseAsArrayOf(parseAsString)nuqsMulti-select filters (statuses[], priorities[])
parseAsIntegernuqsNumeric params (page, limit)
parseAsBooleannuqsToggles
parseAsIsoDatenuqsDate range pickers
Always call .withDefault(value) to avoid null returns on first load.

CSV export — filter-aware

When a page exports data, the export should respect the current filter state. Since the filtered list is already computed, pass it directly to the export function:
audit/page.tsx
function exportToCsv(events: AuditEvent[]) {
  const rows = events.map((e) =>
    [
      e.timestamp,
      e.actor.name,
      e.action,
      e.resource.type,
      e.resource.name,
      e.severity,
      e.status,
    ].join(',')
  );
  const blob = new Blob(
    [['timestamp,actor,action,resource_type,resource_name,severity,status', ...rows].join('\n')],
    { type: 'text/csv' }
  );
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'audit-log.csv';
  a.click();
}

// In JSX — receives the already-filtered array, not the full dataset
<Button onClick={() => exportToCsv(filteredEvents)}>Export CSV</Button>;
The exported file always matches what the user sees, not the raw unfiltered API response.

Adding nuqs to a new page

  1. Import useQueryState and the right parseAs* primitive.
  2. Replace useState calls for filter values with useQueryState.
  3. Keep the value in queryKey if it drives an API call.
  4. Done — no adapter changes, no router wiring, no context setup.
// Before
const [status, setStatus] = useState('all');

// After — one import, one change
import { parseAsString, useQueryState } from 'nuqs';
const [status, setStatus] = useQueryState('status', parseAsString.withDefault('all'));
Caution: Do not mix useState and useQueryState for the same filter. If a filter is URL-persistent, it must only use useQueryState. Mixing the two creates split-brain state where the URL and component disagree after navigation.

Adding nuqs to a new portal

  1. Add nuqs to the app’s package.json dependencies (it is already a workspace root dep).
  2. Import NuqsAdapter from nuqs/adapters/next/app in the app’s providers.tsx.
  3. Wrap children with <NuqsAdapter>.
  4. All useQueryState calls in the app will work automatically.
For Vite-based SPAs (e.g. apps/workbench) use nuqs/adapters/react instead.