

Use `useUrlState` to keep component state synchronized with a single URL parameter. This is ideal for inputs, toggles, tabs, pagination, and other value controls.

`useUrlState` is route-agnostic: it reads the current location search params through TanStack Router and validates only the parameter it owns through its codec.

## Usage [#usage]

```tsx
import { useUrlState } from "@tilt-legal/cubitt-components/utilities/hooks";

export function SearchInput() {
  const { value, setValue } = useUrlState({
    paramName: "search",
    paramDefaultValue: "",
  });

  return (
    <input
      value={value ?? ""}
      onChange={(event) => void setValue(event.target.value)}
    />
  );
}
```

## Router registration [#router-registration]

For the best TypeScript experience, make sure your app has TanStack Router registration in place. With that, `paramName` narrows to known search keys from your route tree instead of falling back to plain `string`.

TanStack Start file-based apps usually get this automatically from generated route types. Plain TanStack Router apps should use the standard `Register` declaration once in app setup.

### Escape hatch [#escape-hatch]

If you intentionally need a `paramName` outside your registered route tree, wrap it with `untypedUrlStateParamName(...)`.

```tsx
import {
  untypedUrlStateParamName,
  useUrlState,
} from "@tilt-legal/cubitt-components/utilities/hooks";

export function DebugPanelInput() {
  const { value, setValue } = useUrlState({
    paramName: untypedUrlStateParamName("debug-panel"),
    paramDefaultValue: "",
  });

  return (
    <input
      value={value ?? ""}
      onChange={(event) => void setValue(event.target.value)}
    />
  );
}
```

<Callout type="warning" title="Lost type safety">
  This helper explicitly opts that usage out of TanStack Router key checking.
  TypeScript can no longer autocomplete known keys or catch typos and search-key
  refactors for that parameter. Prefer proper router registration whenever you
  can.
</Callout>

## API [#api]

| Option                | Type                         | Description                                                                      | Default |
| --------------------- | ---------------------------- | -------------------------------------------------------------------------------- | ------- |
| `paramName`           | `string`                     | URL parameter name. Narrows to known search keys when your router is registered. | -       |
| `paramValue`          | `T`                          | Controlled value (overrides URL state)                                           | -       |
| `paramDefaultValue`   | `T`                          | Default value when the URL is empty                                              | -       |
| `onUrlValueChange`    | `(value: T \| null) => void` | Callback when the URL value changes                                              | -       |
| `paramClearOnDefault` | `boolean`                    | Remove the parameter when the value equals default                               | `true`  |
| `paramThrottle`       | `number`                     | Throttle URL writes in milliseconds                                              | -       |
| `paramDebounce`       | `number`                     | Debounce URL writes in milliseconds                                              | -       |
| `paramCodec`          | `UrlStateCodec<T>`           | Custom codec for advanced data shapes (overrides automatic selection)            | auto    |

## Type support [#type-support]

When you provide a default or controlled value, the hook infers the codec:

* `string` → `stringCodec`
* `boolean` → `booleanCodec`
* `number` → `numberCodec`
* `string[]` → `stringArrayCodec` with implicit `[]`
* `number[]` → `numberArrayCodec` with implicit `[]`
* objects → `createJsonCodec<T>(validator)`

If you need a custom shape, pass `paramCodec`.

If your app's TanStack Router serializer leaves complex values as raw strings, prefer an explicit `paramCodec` built with `createJsonCodec(..., { parseRawString })` rather than relying on any implicit fallback.

## Validation boundaries [#validation-boundaries]

`useUrlState` only validates the single parameter it owns through its codec. Route-level search validation still belongs to the consuming app via TanStack Router `validateSearch`.
