npm package: @ivancerovina/contracts-react
Description: React hooks for @ivancerovina/contracts WebSocket namespaces — typed Socket.IO via Jotai atom families
Author: Ivan Cerovina
Latest version: 1.0.0

# @ivancerovina/contracts-react

React hooks for `@ivancerovina/contracts` WebSocket namespaces. Typed Socket.IO connections managed by Jotai atom families — connections open when the first component subscribes and close when the last one unmounts.

## Install

```bash
pnpm add @ivancerovina/contracts-react @ivancerovina/contracts jotai socket.io-client react
```

`react` and `jotai` must be wrapped in a `Provider` at the app root.

## Setup

Call `createContractSocket` once with your server URL. This returns the hooks bound to that URL.

```typescript
import { createContractSocket } from "@ivancerovina/contracts-react";

export const { useContractSocket, useContractSocketEvent } =
  createContractSocket("http://localhost:3000");
```

## Defining events in contracts

Events are defined in the `ws` array of a contract. Each event is either a bare Zod schema (no ack) or `{ data, ack }` for acknowledgement support.

```typescript
import { createContract } from "@ivancerovina/contracts";
import { z } from "zod";

const taskSchema = z.object({
  id: z.string().uuid(),
  title: z.string(),
  status: z.enum(["todo", "in_progress", "done"]),
});

export const TaskContract = createContract({
  name: "Tasks",
  description: "Task management",
  baseRoute: "/tasks",
  errors: {},
  routes: { /* ... */ },
  ws: [
    {
      namespace: "/tasks",
      serverEvents: {
        // Bare schema — no ack
        taskCreated: taskSchema,

        // Ack without args
        taskUpdated: { data: taskSchema, ack: z.undefined() },

        // Ack with typed response
        requestSync: {
          data: z.object({ since: z.string().datetime() }),
          ack: z.object({ synced: z.boolean(), count: z.number() }),
        },
      },
      clientEvents: {
        subscribeToProject: z.object({ projectId: z.string().uuid() }),
        unsubscribeFromProject: z.object({ projectId: z.string().uuid() }),
      },
    },
  ],
});
```

## `useContractSocket`

Low-level hook. Returns a typed socket handle with `on`, `emit`, `connected`, and the raw `socket`.

```typescript
function TaskList() {
  const tasks = useContractSocket(TaskContract, "/tasks");
  //                                            ^ autocompletes to contract namespace strings

  useEffect(() => {
    const unsub = tasks.on("taskCreated", (event) => {
      //                    ^ autocompletes to server event names
      console.log(event.data.title);
      //          ^ typed: { id: string; title: string; status: ... }
    });
    return unsub;
  }, [tasks.on]);

  // Emit client events — payload is typed
  tasks.emit("subscribeToProject", { projectId: "abc-123" });

  // Connection state
  if (tasks.connected) { /* ... */ }

  return null;
}
```

### `on()` handler argument

The handler receives an object, not raw data:

| Event definition | Handler argument |
|-----------------|-----------------|
| `taskCreated: taskSchema` | `{ data: Task }` |
| `taskUpdated: { data: taskSchema, ack: z.undefined() }` | `{ data: Task; ack: () => void }` |
| `requestSync: { data: ..., ack: responseSchema }` | `{ data: ...; ack: (response: Response) => void }` |

`ack` only exists on the object when the event definition includes it.

## `useContractSocketEvent`

Subscribes to a single server event with automatic lifecycle management and Zod validation. No `useEffect` boilerplate needed.

```typescript
function TaskFeed() {
  // No ack — event has { data }
  useContractSocketEvent(TaskContract, "/tasks", "taskCreated", (event) => {
    console.log(event.data.title);
    //          ^ Zod-validated and typed
  });

  return <div>...</div>;
}
```

### Ack support

```typescript
// Ack without params
useContractSocketEvent(TaskContract, "/tasks", "taskUpdated", (event) => {
  console.log(event.data.title);
  event.ack(); // no args
});

// Ack with typed params
useContractSocketEvent(TaskContract, "/tasks", "requestSync", (event) => {
  console.log(event.data.since);
  event.ack({ synced: true, count: 42 });
  //         ^ typed: { synced: boolean; count: number }
});
```

### Emitting from the handler

The handler receives `event.socket` — the full typed socket for the namespace. Use it to emit client events without needing a separate `useContractSocket` call.

```typescript
useContractSocketEvent(TaskContract, "/tasks", "taskCreated", (event) => {
  console.log(event.data.title);

  // Emit from inside the handler
  event.socket.emit("subscribeToProject", { projectId: "abc-123" });
  //                 ^ autocompletes to client event names

  // Full socket access
  event.socket.connected;
  event.socket.on("taskUpdated", (e) => { /* ... */ });
});
```

## Connection lifecycle

Connections are managed by a Jotai `atomFamily` keyed by namespace:

- **First component** subscribing to a namespace opens the WebSocket connection
- **Multiple components** using the same namespace share one connection
- **Last component** unmounting disconnects the socket and removes it from the cache

No manual connect/disconnect needed.

## Exported types

| Type | Description |
|------|-------------|
| `WsNamespaces<C>` | Union of namespace strings from a contract |
| `FindNamespace<C, N>` | Extract a specific `NamespaceDefinition` by namespace string |
| `ServerEvents<NS>` | Union of server event names from a namespace |
| `EventHandlerArg<E>` | The typed `{ data, ack? }` object for an event |
| `TypedSocket<NS>` | The socket handle with typed `on`, `emit`, `connected`, `socket` |

## Scripts

```bash
pnpm build       # Build with tsdown
pnpm dev         # Watch mode
pnpm lint        # Biome check
```