npm package: @qds.dev/base
Description: Create a Qwik library
Latest version: 0.14.3

# @qds.dev/base

**Shared utilities and bindings system for Qwik Design System**

[![npm version](https://img.shields.io/npm/v/@qds.dev/base.svg?style=flat-square)](https://www.npmjs.com/package/@qds.dev/base)

## Overview

Building reactive components requires consistent patterns for state management, prop handling, and signal/value normalization. Rolling your own leads to inconsistency and bugs across components.

**@qds.dev/base** provides the bindings system (`bind:*` props), shared hooks, state utilities, and type definitions that power all QDS components. It handles signal/value normalization so you can pass either `value` or `bind:value` to any QDS component without worrying about the difference.

This package is the shared layer of QDS. While you can use it directly when building your own components, you'll typically interact with it indirectly through `@qds.dev/ui`, `@qds.dev/motion`, or `@qds.dev/code`.

## Installation

```bash
npm install @qds.dev/base
```

**Peer dependencies:**

- `@qwik.dev/core`

## Quick Start

### Using the bindings hook

The most common use case is normalizing signal/value props in your components:

```tsx
import { useBindings } from "@qds.dev/base";
import { component$, type Signal } from "@qwik.dev/core";

// now typed color and bind:color props
type MyProps = BindableProps<{ color: string }>;

// handles value or bind:value passed to it. Add a new property each time to bind
export const MyColor = component$<MyProps>((props) => {
  const { colorSig: color } = useBindings(props, { value: "purple" });

  return (
    <div>
      {/* initially, purple, can change both internally and externally */}
      {color.value}
    </div>
  );
});
```

## API

@qds.dev/base provides utilities through multiple entry points:

| Entry Point       | Exports                                                                                                                                                   |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Main** (`.`)    | Re-exports all symbols from `/state`, `/helpers`, `/hooks`, `/playground`, `/types`                                                                       |
| **`/state`**      | `useBindings`, `BindableProps`, `SignalResults`, `destructureBindings`, `useBoundSignal`, `mergeRefs`, `useStoreSignal`                                   |
| **`/helpers`**    | `appendId`, `removeId`, `toggleId`, `hasIdInList`, `getNextEnabledIndex`, `getPrevEnabledIndex`, `hashString`, `createCache`, `Cache`, `createSymbolName` |
| **`/hooks`**      | `useDebouncer`, `useResumed$`, `useResumedQrl`, `useMountTask$`, `useMountTaskQrl`                                                                        |
| **`/playground`** | `usePlayground`, `livePropsContextId`, `PlaygroundEvent`, `LivePropsContext` (internal - for playground/REPL)                                             |
| **`/types`**      | `addWindowEventListener`, `removeWindowEventListener`                                                                                                     |

You can import from main (`.`) or specific subpath. Both work:

- `import { useBindings } from '@qds.dev/base'`
- `import { useBindings } from '@qds.dev/base/state'`

## For Contributors: When to Use Each Export

**Quick answers to "I need to..."**

### State Management

| Need                                      | Use                   | From     |
| ----------------------------------------- | --------------------- | -------- |
| Normalize value/signal props in component | `useBindings`         | `/state` |
| Two-way bind to optional signal           | `useBoundSignal`      | `/state` |
| Remove bind:\* props before spreading     | `destructureBindings` | `/state` |
| Combine multiple refs into one            | `mergeRefs`           | `/state` |
| Create signal from store property         | `useStoreSignal`      | `/state` |

### ARIA and Accessibility

| Need                                 | Use           | From       |
| ------------------------------------ | ------------- | ---------- |
| Add ID to aria-describedby list      | `appendId`    | `/helpers` |
| Remove ID from aria-describedby list | `removeId`    | `/helpers` |
| Toggle ID in aria-describedby list   | `toggleId`    | `/helpers` |
| Check if ID in space-separated list  | `hasIdInList` | `/helpers` |

### Keyboard Navigation

| Need                               | Use                   | From       |
| ---------------------------------- | --------------------- | ---------- |
| Find next enabled item in list     | `getNextEnabledIndex` | `/helpers` |
| Find previous enabled item in list | `getPrevEnabledIndex` | `/helpers` |

### Lifecycle Hooks

| Need                                | Use             | From     |
| ----------------------------------- | --------------- | -------- |
| Debounce function calls             | `useDebouncer`  | `/hooks` |
| Run code when component resumes     | `useResumed$`   | `/hooks` |
| Run code once on mount with cleanup | `useMountTask$` | `/hooks` |

### Utilities

| Need                                          | Use                         | From       |
| --------------------------------------------- | --------------------------- | ---------- |
| Hash string to short ID                       | `hashString`                | `/helpers` |
| Create LRU cache                              | `createCache`               | `/helpers` |
| Generate Qwik-compatible symbol name          | `createSymbolName`          | `/helpers` |
| Add window event listener (QRL-compatible)    | `addWindowEventListener`    | `/types`   |
| Remove window event listener (QRL-compatible) | `removeWindowEventListener` | `/types`   |

### Playground (Internal)

| Need                                 | Use                  | From          |
| ------------------------------------ | -------------------- | ------------- |
| Build interactive component examples | `usePlayground`      | `/playground` |
| Access live props context            | `livePropsContextId` | `/playground` |

**Note:** `/playground` exports are for internal QDS documentation. Most consumers won't need them.

### Usage Examples

#### destructureBindings - Remove bind:\* props before spreading

```tsx
import { destructureBindings, useBindings } from "@qds.dev/base/state";
import { component$ } from "@qwik.dev/core";

export const Input = component$((props) => {
  const { valueSig } = useBindings(props, { value: "" });
  const remaining = destructureBindings(props, { value: "" });

  return <input {...remaining} value={valueSig.value} />;
});
```

#### appendId/removeId - Manage ARIA relationships

```tsx
import { appendId, removeId } from "@qds.dev/base/helpers";
import { useMountTask$ } from "@qds.dev/base/hooks";
import { component$, type PropsOf, Slot, useContext, useSignal } from "@qwik.dev/core";
import { Render } from "../render/render";
import { checkboxContextId } from "./checkbox-context";

type PublicCheckboxDescriptionProps = PropsOf<"div">;
/** A component that renders the description text for a checkbox */
export const CheckboxDescription = component$((props: PublicCheckboxDescriptionProps) => {
  const context = useContext(checkboxContextId);
  const descriptionId = `${context.localId}-description`;
  const descriptionRef = useSignal<HTMLDivElement>();

  useMountTask$(({ cleanup }) => {
    context.describedByIds.value = appendId(context.describedByIds.value, descriptionId);

    cleanup(() => {
      context.describedByIds.value = removeId(
        context.describedByIds.value,
        descriptionId
      );
    });
  }, descriptionRef);

  return (
    // Identifier for the checkbox description element
    <Render
      fallback="div"
      id={descriptionId}
      ui-qds-checkbox-description
      {...props}
      internalRef={descriptionRef}
    >
      <Slot />
    </Render>
  );
});
```

#### getNextEnabledIndex - Keyboard navigation in lists

```tsx
import { getNextEnabledIndex } from "@qds.dev/base/helpers";
import { component$, useSignal, useContext } from "@qwik.dev/core";
import { checkboxContextId } from "./checkbox-context";

export const Demo = component$(() => {
  const context = useContext(checkboxContextId);
  const focusedIndex = useSignal(0);
  const handleArrowDown = $(() => {
    const nextIndex = getNextEnabledIndex(context.numItems, focusedIndex.value);
    if (nextIndex !== -1) focusedIndex.value = nextIndex;
  });

  return <button onKeyDown$={handleArrowDown}>{/* ... */}</button>;
});
```

See detailed API below for function signatures and complete documentation.

## Detailed API Reference

### /state - State Management

#### useBindings

Normalizes value and signal props into a consistent signal interface.

```typescript
function useBindings<T extends object>(
  props: BindableProps<T>,
  initialValues: T
): SignalResults<T>;
```

**Parameters:**

- `props` - Component props with value or bind:value variants
- `initialValues` - Default values for each bindable property

**Returns:** Object with signals (suffixed with `Sig`) for each property

**Example:**

```tsx
const { valueSig: value, disabledSig: isDisabled } = useBindings(props, {
  value: "",
  disabled: false
});
```

#### destructureBindings

Removes bindable props from props object before spreading to DOM elements.

```typescript
function destructureBindings<T extends object, Props extends BindableProps<T>>(
  props: Props,
  initialValues: T
): Omit<Props, keyof T | `bind:${keyof T}`>;
```

**Use when:** You need to spread remaining props to a DOM element after extracting bindings.

#### useBoundSignal

Creates a signal bound to an optional external signal.

```typescript
function useBoundSignal<T>(
  givenSignal?: Signal<T>,
  initialValue?: T,
  valueBasedSignal?: Signal<T | undefined>
): Signal<T>;
```

**Use when:** You need simpler two-way binding than full useBindings.

#### mergeRefs

Combines multiple refs into a single ref function.

```typescript
function mergeRefs<T extends Element>(
  ...refs: (
    | Signal<Element | undefined>
    | Signal<T | undefined>
    | ((el: T) => void)
    | undefined
  )[]
): (el: T) => void;
```

**Use when:** Component needs both internal ref and forwarded ref.

#### useStoreSignal

Creates a signal from a store property with two-way synchronization.

```typescript
function useStoreSignal<T>(
  store: Record<string, unknown>,
  key: keyof typeof store
): Signal<T>;
```

**Use when:** You need a signal that stays synchronized with a Qwik store property.

### /helpers - DOM Utilities

#### appendId

Appends an ID to a space-separated string of IDs, avoiding duplicates.

```typescript
function appendId(ids: string | undefined | null, idToAdd: string): string;
```

**Use for:** Managing `aria-describedby`, `aria-labelledby`, or similar attributes.

#### removeId

Removes an ID from a space-separated string of IDs.

```typescript
function removeId(ids: string | undefined | null, idToRemove: string): string | undefined;
```

**Returns:** Updated string, or `undefined` if result is empty.

#### toggleId

Toggles an ID in a space-separated string of IDs.

```typescript
function toggleId(ids: string | undefined | null, idToToggle: string): string | undefined;
```

**Use for:** Toggling error messages or help text visibility.

#### hasIdInList

Checks if an ID exists in a space-separated list.

```typescript
function hasIdInList(idList: string | undefined, targetId: string): boolean;
```

#### getNextEnabledIndex

Finds the next enabled item in a list.

```typescript
function getNextEnabledIndex<
  E extends { disabled?: boolean } = HTMLButtonElement
>(options: {
  items: ItemWithPotentialDisabledRef<E>[];
  currentIndex: number;
  loop?: boolean; // defaults to true
}): number;
```

**Returns:** Index of next enabled item, or `-1` if none found.

**Use for:** Arrow key navigation in menus, tabs, listboxes.

#### getPrevEnabledIndex

Finds the previous enabled item in a list.

```typescript
function getPrevEnabledIndex<
  E extends { disabled?: boolean } = HTMLButtonElement
>(options: {
  items: ItemWithPotentialDisabledRef<E>[];
  currentIndex: number;
  loop?: boolean; // defaults to true
}): number;
```

**Returns:** Index of previous enabled item, or `-1` if none found.

#### hashString

Generates a fast, non-cryptographic hash from a string.

```typescript
function hashString(str: string): string;
```

**Returns:** Base-36 encoded hash string.

**Use for:** Generating short IDs, cache keys, or DOM element IDs.

#### createCache

Creates an LRU cache with a maximum size.

```typescript
function createCache<T>(maxSize: number = 100): Cache<T>;

type Cache<T> = {
  get(key: string): T | undefined;
  set(key: string, value: T): void;
  has(key: string): boolean;
  clear(): void;
  size: number;
};
```

**Use for:** Memoizing expensive computations or DOM references.

#### createSymbolName

Generates a Qwik-compatible symbol name from a string.

```typescript
function createSymbolName(input: string): string;
```

**Returns:** Symbol name in format `s_` followed by 11 hex characters.

**Use for:** Creating symbols that integrate with Qwik's QRL system.

### /hooks - Lifecycle Hooks

#### useDebouncer

Creates a debounced function that delays invocation.

```typescript
function useDebouncer(
  fn: QRL<(args: any) => void>,
  delay: number
): QRL<(args?: any) => void>;
```

**Use for:** Debouncing search input, resize handlers, or scroll events.

#### useResumed$

Runs code when component resumes (on client after interaction or navigation).

```typescript
function useResumed$(callback: () => void): void;
```

**Use for:** Adding event listeners, starting animations, or initializing browser-only features.

#### useResumedQrl

QRL variant of `useResumed$`. Optimizer transforms `useResumed$` into `useResumedQrl`.

```typescript
function useResumedQrl(qrl: QRL<() => void>): void;
```

#### useMountTask$

Runs code once on mount with optional cleanup function.

```typescript
function useMountTask$(
  taskFn: (ctx: { cleanup: (cleanupFn: () => void) => void }) => void,
  elementRef: Signal<Element | undefined>
): void;
```

**Use for:** Setting up subscriptions, intervals, or event listeners that need cleanup.

#### useMountTaskQrl

QRL variant of `useMountTask$`. Optimizer transforms `useMountTask$` into `useMountTaskQrl`.

```typescript
function useMountTaskQrl(
  taskFn: QRL<(ctx: { cleanup: (cleanupFn: () => void) => void }) => void>,
  elementRef: Signal<Element | undefined>
): void;
```

### /types - Window Event Helpers

#### addWindowEventListener

Adds a window event listener with QRL handler support.

```typescript
function addWindowEventListener<E extends Event = Event>(
  type: string,
  handler: QRL<(event: E) => void | Promise<void>>,
  options?: boolean | AddEventListenerOptions
): void;
```

**Use when:** You need to conditionally add window event listeners (e.g., drag handlers).

#### removeWindowEventListener

Removes a window event listener with QRL handler support.

```typescript
function removeWindowEventListener<E extends Event = Event>(
  type: string,
  handler: QRL<(event: E) => void | Promise<void>>,
  options?: boolean | EventListenerOptions
): void;
```

### /playground - Playground (Internal)

#### usePlayground

Creates an interactive playground context for component examples.

```typescript
function usePlayground<T extends Record<string, unknown>>(
  /* parameters omitted - internal API */
): /* returns omitted - internal API */
```

**Note:** This is for internal QDS documentation. Most consumers won't need it.

#### livePropsContextId

Context ID for accessing live props in playground.

```typescript
const livePropsContextId: ContextId<LivePropsContext>;
```

**Note:** Internal use only.

## Architecture

For package internals, dependency relationships, and design decisions (including the useBindings pattern rationale), see [ARCHITECTURE.md](./ARCHITECTURE.md).

## Why This Pattern

### The bindings system

QDS components accept props like `bind:checked` alongside standard `checked` props. This pattern lets you:

- **Pass values directly**: `<Checkbox checked={true} />`
- **Pass signals for reactivity**: `<Checkbox bind:checked={mySignal} />`
- **Let components handle it**: The component uses `useBindings` to normalize both cases into a consistent internal API

This eliminates the controlled/uncontrolled component distinction from React. You don't need separate `value`/`defaultValue` props or `onChange` callbacks. The bindings system handles synchronization automatically.

### Why base is separate

`@qds.dev/base` is not part of `@qds.dev/ui` because it provides foundation utilities that can be used independently:

- **@qds.dev/ui** - Uses `@qds.dev/base` as a devDependency (bundled at build time)

By keeping shared utilities in `@qds.dev/base`, we enable consistent state management patterns and allow consumers to use the bindings system independently if needed.

## Related Packages

**Depends on:** None (this is the foundation package)

**Used by:**

- [@qds.dev/ui](https://www.npmjs.com/package/@qds.dev/ui) - Headless UI components (devDependency - bundled at build time)