# React GraphQL Library

[![CI](https://github.com/PhillipAWells/workspace/actions/workflows/ci.yml/badge.svg)](https://github.com/PhillipAWells/workspace/actions/workflows/ci.yml)
[![npm version](https://img.shields.io/npm/v/@pawells/react-graphql.svg)](https://www.npmjs.com/package/@pawells/react-graphql)
[![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)

## Description

`@pawells/react-graphql` is an Apollo Client (v4) setup library for React applications. It configures HTTP and WebSocket (`graphql-ws`) transports on a single client, tracks connection state across the `Connecting → Connected → (Reconnecting | Error | Disconnected)` lifecycle, and provides automatic reconnection with exponential backoff. A `GraphQLProvider` React component wraps `ApolloProvider` and makes connection state and manual reconnection available to the component tree via React context.

Both endpoint URIs are validated at client-creation time: `wss://` is required for `wsUri` in production, and `https://` is required for `httpUri` in production. Both requirements are waived when the hostname resolves to localhost (`localhost`, `127.0.0.1`, or `::1`). Passing an insecure `ws://` URL causes `createGraphQLClient` to throw a `GraphQLClientError` with code `GRAPHQL_INSECURE_WEBSOCKET`; passing an insecure `http://` URL throws a `GraphQLClientError` with code `GRAPHQL_INSECURE_HTTP`.

## Requirements

- **Node** >= 22.0.0
- **Peer dependencies** — install alongside this package:
  - `@apollo/client` >= 4.0.0
  - `graphql` ^16.0.0
  - `react` >= 19.0.0
  - `react-dom` >= 19.0.0
  - `rxjs` >= 7.0.0

## Installation

```sh
npm install @pawells/react-graphql @apollo/client graphql react react-dom rxjs
```

## Quick Start

Wrap your application root with `GraphQLProvider`, passing an `IGraphQLClientOptions` object.

```tsx
import { GraphQLProvider } from '@pawells/react-graphql';

const graphqlOptions = {
  name: 'my-app',
  httpUri: 'https://api.example.com/graphql',
  wsUri: 'wss://api.example.com/graphql/ws',
  token: () => localStorage.getItem('auth-token') ?? '',
  logGraphQLErrors: true,
  logNetworkErrors: true,
};

export function Root() {
  return (
    <GraphQLProvider options={graphqlOptions} fallback={<p>Connecting...</p>}>
      <App />
    </GraphQLProvider>
  );
}
```

Use `useConnectionState` inside any descendant to read the live connection state:

```tsx
import { useConnectionState, GraphQLConnectionState } from '@pawells/react-graphql';

export function ConnectionBanner() {
  const state = useConnectionState();

  if (state === GraphQLConnectionState.Reconnecting) {
    return <p>Reconnecting to server...</p>;
  }
  if (state === GraphQLConnectionState.Error) {
    return <p>Connection error.</p>;
  }
  return null;
}
```

## API Reference

### Provider

#### `GraphQLProvider`

```tsx
function GraphQLProvider(props: IGraphQLProviderProps): React.ReactElement
```

React component that creates an Apollo Client from `options`, mounts an `ApolloProvider`, and publishes connection state to `GraphQLContext`. Renders `fallback` (or nothing) until the client is ready. Disposes the client on unmount.

```ts
interface IGraphQLProviderProps {
  options: IGraphQLClientOptions; // Client configuration
  children: React.ReactNode;      // Application subtree
  fallback?: React.ReactNode;     // Optional placeholder during initialization
}
```

Pass a stable `options` reference (defined outside the render function or memoized) to prevent unnecessary client recreation.

---

### Factory

#### `createGraphQLClient`

```ts
function createGraphQLClient(options: IGraphQLClientOptions): IGraphQLClientResult
```

Creates a configured Apollo Client with HTTP and WebSocket transport. Configures:

- **Auth** — bearer token injected into the HTTP `Authorization` header and WebSocket `connectionParams`. Accepts a static string or an async factory function.
- **Retry** — `RetryLink` with exponential backoff: initial delay 1 s, max 10 s, jitter enabled, up to 10 attempts.
- **Fetch policy** — queries default to `cache-first`, watched queries to `cache-and-network`, and mutations to `no-cache`.
- **Batching** — HTTP requests use a plain `HttpLink` by default, or `BatchHttpLink` when `options.batchRequests` is `true`.
- **WebSocket** — managed by `graphql-ws` with automatic reconnection on connection drop.
- **Error logging** — optional `console.error` output for GraphQL and network errors (sanitized to omit sensitive fields).

Throws `GraphQLClientError` (code `GRAPHQL_INSECURE_WEBSOCKET`) when `wsUri` uses `ws://` with a non-localhost hostname, and `GraphQLClientError` (code `GRAPHQL_INSECURE_HTTP`) when `httpUri` uses `http://` with a non-localhost hostname.

---

### Hooks

All three hooks must be called inside `GraphQLProvider`.

#### `useGraphQLContext`

```ts
function useGraphQLContext(): IGraphQLContextValue
```

Returns the full context value (`{ connectionState, reconnect }`) published by the nearest `GraphQLProvider`. This is the lower-level hook that `useConnectionState` and `useGraphQLReconnect` wrap — use it directly for advanced/custom-hook use cases that need both values at once. Throws `GraphQLClientError` (code `GRAPHQL_CONTEXT_NOT_FOUND`) when called outside `GraphQLProvider`.

#### `useConnectionState`

```ts
function useConnectionState(): GraphQLConnectionState
```

Returns the current WebSocket connection state. The component re-renders whenever the state changes.

#### `useGraphQLReconnect`

```ts
function useGraphQLReconnect(): () => void
```

Returns a callback that disposes the current client and creates a fresh one, triggering a new connection sequence. Useful for building manual reconnect controls.

---

### Types

#### `IGraphQLClientOptions`

Configuration object passed to `createGraphQLClient` and `GraphQLProvider`.

| Property | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | Yes | Client identifier, forwarded to Apollo DevTools via `devtools.name`. |
| `httpUri` | `string` | Yes | GraphQL HTTP endpoint URI. `https://` required in production; `http://` permitted for localhost only. |
| `wsUri` | `string` | Yes | GraphQL WebSocket endpoint URI. `wss://` required in production; `ws://` permitted for localhost only. |
| `token` | `string \| (() => string \| Promise<string>)` | No | Static bearer token or async token provider. |
| `logGraphQLErrors` | `boolean` | No | Log GraphQL errors to the console. |
| `logNetworkErrors` | `boolean` | No | Log network errors to the console. |
| `cache` | `ApolloCache \| undefined` | No | Apollo cache instance (any `ApolloCache` subtype accepted). A new `InMemoryCache` is created if omitted. |
| `cacheOptions` | `InMemoryCacheConfig` | No | Configuration passed to the auto-created `InMemoryCache` (ignored if `cache` is provided). |
| `batchRequests` | `boolean` | No | Enables HTTP request batching via `BatchHttpLink` instead of `HttpLink`. Defaults to `false`. |
| `persistCache` | `boolean` | No | Reserved — has no effect in the current release. Intended for future cache-persistence support. |

#### `IGraphQLClientResult`

Return value of `createGraphQLClient`.

| Property | Type | Description |
|---|---|---|
| `client` | `ApolloClient` | Configured Apollo Client instance. |
| `dispose` | `TDisposeFunction` | Stops the WebSocket connection and Apollo Client. Call on teardown. |
| `getConnectionState` | `() => GraphQLConnectionState` | Returns the current connection state without subscribing. |
| `onStateChange` | `(handler: (state: GraphQLConnectionState) => void) => () => void` | Subscribes to connection state changes. Returns an unsubscribe function. |

#### `IGraphQLContextValue`

Context value provided by `GraphQLProvider` via React Context.

| Property | Type | Description |
|---|---|---|
| `connectionState` | `GraphQLConnectionState` | Current WebSocket connection state. |
| `reconnect` | `() => void` | Function to manually trigger a reconnection attempt. |

#### `TDisposeFunction`

```ts
type TDisposeFunction = () => void;
```

Cleanup callback that terminates the WebSocket connection and stops the Apollo Client. Called automatically by `GraphQLProvider` on unmount.

#### `GraphQLConnectionState`

Enum representing the WebSocket connection lifecycle.

| Value | String | Description |
|---|---|---|
| `GraphQLConnectionState.Connecting` | `'Connecting'` | Client is establishing the initial connection. |
| `GraphQLConnectionState.Connected` | `'Connected'` | Connection is open and healthy. |
| `GraphQLConnectionState.Reconnecting` | `'Reconnecting'` | Connection dropped unexpectedly; automatic reconnection is in progress. |
| `GraphQLConnectionState.Disconnected` | `'Disconnected'` | Client was intentionally disposed (e.g. `dispose()` called, or `GraphQLProvider` unmounted). |
| `GraphQLConnectionState.Error` | `'Error'` | A connection error has occurred. |

#### `GraphQLClientError`

```ts
class GraphQLClientError extends BaseError<TGraphQLClientErrorMetadata> {
  static readonly Code: {
    INSECURE_WEBSOCKET: 'GRAPHQL_INSECURE_WEBSOCKET';
    INSECURE_HTTP: 'GRAPHQL_INSECURE_HTTP';
    CONTEXT_NOT_FOUND: 'GRAPHQL_CONTEXT_NOT_FOUND';
  };
}
```

Error type thrown by `createGraphQLClient` and the context hooks. Catch it and compare `error.Code` against `GraphQLClientError.Code` to classify the failure:

```ts
try {
  createGraphQLClient(options);
} catch (error) {
  if (error instanceof GraphQLClientError && error.Code === GraphQLClientError.Code.INSECURE_HTTP) {
    console.error('HTTP endpoint must use https:// outside localhost');
  }
}
```

## License

MIT — See [LICENSE](./LICENSE) for details.
