# Reactor

## Overview

`Reactor` is the core class that represents a **reactive connection** to an Internet Computer canister. The name "Reactor" reflects its fundamental purpose: it **reacts** to changes and propagates updates throughout your application.

### The Philosophy

Traditional IC development treats canister calls as one-shot operations—you call, you get data, you're done. But modern applications need more:

- **Data should stay fresh** — automatically refetch stale data in the background
- **Requests should be deduplicated** — multiple components fetching the same data shouldn't make multiple calls
- **Cache should be managed** — know when to invalidate and refetch
- **Errors should be typed** — canister errors are domain-specific, not just strings

`Reactor` wraps a canister connection with **TanStack Query integration**, transforming static canister calls into reactive data streams that your UI components can subscribe to.

```typescript
// A Reactor isn't just a connection — it's a reactive data source
const backend = new Reactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "backend", // Required: Used for logging and environment lookup
  canisterId, // Optional: omitted only where the ic_env cookie is trusted
})

// Data flows reactively: cache → components → automatic updates
const { data, isLoading, refetch } = useReactorQuery({
  reactor: backend,
  functionName: "getUser",
  args: [userId],
})
```

## Why Reactor Instead of Actor?

If you're familiar with the standard `Actor` class from `@icp-sdk/core/agent`, you might wonder why use `Reactor`. Here's why:

**Automatic Caching**
Built-in TanStack Query integration for automatic caching, deduplication, and background updates

**Result Unwrapping**
Automatically unwraps `Result<Ok, Err>` types - no more manual `Ok`/`Err` checking

**Type Transformations**
With `DisplayReactor`, auto-convert BigInt → string, Principal → text for easy UI rendering

**Identity Management**
Shares one agent and identity across all reactors via `ClientManager`

### Feature Comparison

| Feature                | Standard Actor | Reactor                  |
| ---------------------- | -------------- | ------------------------ |
| Type-safe method calls | ✅             | ✅                       |
| Query caching          | ❌             | ✅ Built-in              |
| Automatic refetching   | ❌             | ✅ Background updates    |
| Result unwrapping      | ❌ Manual      | ✅ Automatic             |
| Error typing           | ❌ Generic     | ✅ `CanisterError<E>`    |
| Identity sharing       | ❌ Per-actor   | ✅ Via ClientManager     |
| Query invalidation     | ❌             | ✅ `invalidateQueries()` |

## Import

```typescript
// React users - import from @ic-reactor/react
import { Reactor } from "@ic-reactor/react"

// Non-React users
import { Reactor } from "@ic-reactor/core"
```

## Usage

```typescript
import { Reactor, ClientManager } from "@ic-reactor/react"
import { QueryClient } from "@tanstack/query-core"
import { idlFactory, type _SERVICE } from "./declarations/backend"

const queryClient = new QueryClient()
const clientManager = new ClientManager({ queryClient })

const backend = new Reactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "backend", // Required: Used for logging and environment lookup
  canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai", // Optional: omitted only where the ic_env cookie is trusted
})
```

## Constructor Options

| Option           | Type                   | Required | Description                          |
| ---------------- | ---------------------- | -------- | ------------------------------------ |
| `clientManager`  | `ClientManager`        | Yes      | The ClientManager instance           |
| `idlFactory`     | `IDL.InterfaceFactory` | Yes      | Candid IDL factory from declarations |
| `name`           | `string`               | Yes      | Unique name for state management     |
| `canisterId`     | `string \| Principal`  | No       | The canister ID to connect to        |
| `pollingOptions` | `PollingOptions`       | No       | Custom polling options for updates   |

### Why is `name` required?

The `name` parameter is required for **environment-based resolution**. When `canisterId` is omitted, the Reactor uses this name to look up the canister ID in the `ic_env` cookie (`PUBLIC_CANISTER_ID:<name>`) — but only where that cookie is trusted; see below.

For more details, see the [Canister Environment](https://js.icp.build/core/latest/canister-environment) documentation.

It also serves as a unique identifier for:

1. **Debuggability**: Providing meaningful labels in logs.
2. **Development**: identifying the actor in the `ClientManager` state.

### Why is `canisterId` optional?

The `canisterId` is optional because it can be automatically resolved using the `name` — on a host where the `ic_env` cookie is trusted.

- **Automatic Resolution**: If omitted, the Reactor looks the ID up in the `ic_env` cookie using the `name`.
- **Dynamic Usage**: useful when the canister ID is not known at build time or varies by environment.
- **Only where the cookie is trusted**: a local replica, or a host you opted in with `allowEnvConfig`. Cookies are not origin-isolated, so on any other host — a custom domain or mainnet — the cookie is not read and the constructor throws instead, naming the option to set. Bake the ID in at build time for those, which is what the codegen `canisterId` option does. A substituted canister ID cannot be caught by certificate verification: the attacker names a real canister, and its responses verify against the real root key.

## Properties

| Property        | Type               | Description                    |
| --------------- | ------------------ | ------------------------------ |
| `canisterId`    | `Principal`        | The canister's Principal       |
| `name`          | `string`           | Name of the reactor            |
| `service`       | `IDL.ServiceClass` | The Candid service interface   |
| `agent`         | `HttpAgent`        | The IC HTTP agent (getter)     |
| `queryClient`   | `QueryClient`      | TanStack Query client (getter) |
| `clientManager` | `ClientManager`    | The shared client manager      |

## Methods

### callMethod

Call a canister method directly:

```typescript
const result = await backend.callMethod({
  functionName: "getUser",
  args: ["user-123"],
})
```

#### Parameters

| Option         | Type         | Description                 |
| -------------- | ------------ | --------------------------- |
| `functionName` | `string`     | Name of the canister method |
| `args`         | `array`      | Arguments to pass           |
| `callConfig`   | `CallConfig` | Optional call configuration |

#### Returns

`Promise<ReturnType>` — The method's return value (with Result unwrapping)

---

### generateQueryKey

Generate a TanStack Query cache key for a method call:

```typescript
const queryKey = backend.generateQueryKey(
  {
    functionName: "getUser",
    args: ["user-123"],
  },
  {
    canisterId: otherCanisterId,
    effectiveCanisterId: managementCanisterId,
  }
)
// Returns:
// [otherCanisterId, "getUser", { effectiveTarget: { canisterId: managementCanisterId } }, '["user-123"]']
```

The key is composed as:

```
[resolvedCanisterId, functionName, { effectiveTarget }?, argKey?, ...queryKey]
```

- **`resolvedCanisterId`** — `callConfig.canisterId` when supplied (normalized via `Principal.from(...).toString()`), otherwise the reactor's own canister ID.
- **`{ effectiveTarget }`** — a wrapper object holding either `{ canisterId }` or `{ subnetId }`, built from `callConfig.effectiveTarget` or from `callConfig.effectiveCanisterId`. A `canisterId`-shaped target is **omitted** when it equals `resolvedCanisterId`, so the common case produces no such segment.
- **`argKey`** — args are **one single string segment**, `JSON.stringify(args)` with BigInt values rendered as decimal strings. They are not spread into separate elements, and `args: []` still produces the literal `"[]"`.
- **`...queryKey`** — a custom `queryKey` is spread onto the end; it never replaces the identity prefix.

Use this for:

- `invalidateQueries` in mutations
- Manual cache operations
- Query invalidation

If a query uses `callConfig`, pass the same `callConfig` here so the generated
key matches the cache entry used by `fetchQuery`, `getQueryOptions`, and the
React query wrappers.

---

### getQueryOptions

Get TanStack Query options for a method call:

```typescript
const queryOptions = backend.getQueryOptions({
  functionName: "getUser",
  args: ["user-123"],
  callConfig: { canisterId: otherCanisterId },
})

// Use in loaders
const loader = async () => {
  await queryClient.prefetchQuery(queryOptions)
  return null
}

// Or with useQuery directly
const result = useQuery(queryOptions)
```

#### Returns

Object containing `queryKey` and `queryFn` for TanStack Query.

---

### invalidateQueries

Invalidate all cached queries for this canister:

```typescript
// After a successful mutation, invalidate all queries for this canister
await backend.callMethod({ functionName: "updateProfile", args: [newProfile] })
backend.invalidateQueries()

// Invalidate a cached query that used a per-call canister override
backend.invalidateQueries(
  { functionName: "getUser", args: ["user-123"] },
  { canisterId: otherCanisterId }
)
```

This will mark all queries as stale and trigger a refetch for any active queries.

---

### getServiceInterface

Get the Candid service interface (IDL.ServiceClass):

```typescript
const service = backend.getServiceInterface()
console.log(service._fields) // Array of [methodName, FuncClass]
```

Useful for introspection and codec generation.

---

### setCanisterId

Dynamically update the canister ID for this reactor. This is useful when you want to reuse a reactor instance to interact with different canisters of the same type (e.g., multiple ICRC tokens).

```typescript
// Switch to a different ledger canister
ledgerReactor.setCanisterId("ryjl3-tyaaa-aaaaa-aaaba-cai")

// Or use a Principal
ledgerReactor.setCanisterId(Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai"))
```

#### Parameters

| Option       | Type                  | Description         |
| ------------ | --------------------- | ------------------- |
| `canisterId` | `string \| Principal` | The new canister ID |

**Note:** No cache cleanup is needed after switching. Query keys start with the canister
  ID, so the two canisters never share a cache entry: mounted hooks — including
  the infinite-query variants — recompute their key and fetch the new canister's
  data **on their next render**, while the previous canister's entries stay
  cached under their own keys (until normal `gcTime` eviction) and are reused if
  you switch back. `setCanisterId` does not itself trigger a render, so pair it
  with something that does — a route navigation (as in the loader example below)
  or a state update. A hook that renders before then still reads the previous
  canister's key.

---

## Examples

### Complete Setup

```typescript
// src/reactor/index.ts
import { ClientManager, Reactor } from "@ic-reactor/react"
import { QueryClient } from "@tanstack/query-core"
import { idlFactory, type _SERVICE } from "../declarations/backend"

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,
      gcTime: 5 * 60_000,
    },
  },
})

export const clientManager = new ClientManager({
  queryClient,
})

export const backend = new Reactor<_SERVICE>({
  clientManager,
  idlFactory,
  canisterId: import.meta.env.VITE_BACKEND_CANISTER_ID,
  name: "backend",
})
```

### Direct Method Calls

```typescript
// Query method
const user = await backend.callMethod({
  functionName: "getUser",
  args: ["user-123"],
})

// Update method
const result = await backend.callMethod({
  functionName: "createPost",
  args: [{ title: "Hello", content: "World" }],
})
```

### Prefetching in Loaders

```typescript
// React Router loader
export async function loader({ params }) {
  const queryOptions = backend.getQueryOptions({
    functionName: "getUser",
    args: [params.userId],
  })

  await queryClient.prefetchQuery(queryOptions)

  return null
}
```

### Cache Manipulation

```typescript
import { useQueryClient } from "@tanstack/react-query"

function useUpdateUserCache() {
  const queryClient = useQueryClient()

  const updateUser = (userId: string, updates: Partial<User>) => {
    const queryKey = backend.generateQueryKey({
      functionName: "getUser",
      args: [userId],
    })

    queryClient.setQueryData(queryKey, (old: User) => ({
      ...old,
      ...updates,
    }))
  }

  return updateUser
}
```

### Multiple Canisters

```typescript
import { Reactor, ClientManager } from "@ic-reactor/react"
import {
  idlFactory as backendIdl,
  type _SERVICE as BackendService,
} from "../declarations/backend"
import {
  idlFactory as ledgerIdl,
  type _SERVICE as LedgerService,
} from "../declarations/ledger"

// One ClientManager, shared across all reactors
export const backend = new Reactor<BackendService>({
  clientManager,
  idlFactory: backendIdl,
  canisterId: import.meta.env.VITE_BACKEND_CANISTER_ID,
  name: "backend",
})

export const ledger = new Reactor<LedgerService>({
  clientManager,
  idlFactory: ledgerIdl,
  canisterId: import.meta.env.VITE_LEDGER_CANISTER_ID,
  name: "ledger",
})
```

### Dynamic Canister Switching

Use `setCanisterId` to reuse a single reactor instance for multiple canisters of the same type. This is useful for multi-token wallets or canister explorers:

```typescript
import { createFileRoute } from "@tanstack/react-router"
import { ledgerReactor } from "@/canisters/ledger/reactor"
import { icrc1NameQuery, icrc1SymbolQuery } from "@/canisters/ledger/hooks"

export const Route = createFileRoute("/wallet/$canisterId")({
  component: TokenWallet,
  loader: async ({ params: { canisterId } }) => {
    // Switch reactor to target the canister from URL
    ledgerReactor.setCanisterId(canisterId)

    // Prefetch token info
    await Promise.all([icrc1NameQuery.fetch(), icrc1SymbolQuery.fetch()])

    return {}
  },
})

function TokenWallet() {
  // Queries automatically use the canister set by setCanisterId
  const { data: name } = icrc1NameQuery.useQuery()
  const { data: symbol } = icrc1SymbolQuery.useQuery()

  return (
    <div>
      <h1>
        {name} ({symbol})
      </h1>
    </div>
  )
}
```

### fetchQuery

Fetch data from the canister and cache it using React Query:

```typescript
const result = await backend.fetchQuery({
  functionName: "getUser",
  args: ["user-123"],
  callConfig: { canisterId: otherCanisterId },
})
```

This method ensures the data is in the cache (fetching it if necessary) and returns it. It respects the standard React Query caching behavior.

---

### getQueryData

Get the current data from the cache without fetching:

```typescript
const user = backend.getQueryData(
  {
    functionName: "getUser",
    args: ["user-123"],
  },
  { canisterId: otherCanisterId }
)

if (user) {
  console.log("User in cache:", user.name)
}
```

---

## Extending Reactor

You can extend the `Reactor` class to add custom functionality, logging, or middleware. The `callMethod` is now a class method, allowing you to override it easily.

```typescript
class LoggingReactor extends Reactor<BackendService> {
  // Override callMethod to add logging
  async callMethod(params) {
    console.log("Calling method:", params.functionName)
    const result = await super.callMethod(params)
    console.log("Method result:", result)
    return result
  }

  // Override fetchQuery to add custom logic (e.g. auth checks)
  async fetchQuery(params) {
    const principal = await this.clientManager.getUserPrincipal()
    if (principal.isAnonymous()) {
      console.warn("User not authenticated")
    }
    return super.fetchQuery(params)
  }
}

const backend = new LoggingReactor({
  clientManager,
  idlFactory,
  name: "backend",
  canisterId,
})
```

By extending `Reactor`, all methods relying on it (including `callMethod`, `fetchQuery`, and even `createQuery` consumers) will automatically use your custom logic.

## Automatic Result Unwrapping

IC Reactor automatically unwraps Candid `Result` types (`variant { Ok: T; Err: E }`):

- **On `Ok`**: Returns the success value directly
- **On `Err`**: Throws a `CanisterError` containing the error variant

```typescript
import { CanisterError } from "@ic-reactor/react"

// Canister returns: Result<User, CreateUserError>
try {
  const user = await backend.callMethod({
    functionName: "createUser",
    args: [{ name: "Alice", email: "alice@example.com" }],
  })
  // user is User directly (not { Ok: User })
  console.log("Created:", user.name)
} catch (error) {
  if (error instanceof CanisterError) {
    // error.err is the CreateUserError variant
    console.log("Error code:", error.code)
    if ("EmailAlreadyExists" in error.err) {
      console.log("Email already taken")
    }
  }
}
```

This eliminates the need to manually check for `Ok`/`Err` variants in your code.

**Tip:** Learn how to handle canister errors effectively in the [Error
  Handling](https://ic-reactor.b3pay.net/v3/guides/error-handling) guide.

## Notes

**Tip:** Use the `name` option for better debugging. It appears in console logs and
  helps identify which canister is being called.

**Note:** For automatic type transformations (BigInt → string, Principal → text, etc.),
  use [DisplayReactor](https://ic-reactor.b3pay.net/v3/reference/displayreactor) instead.

## Validation

For argument validation before canister calls, use `DisplayReactor` which includes
built-in validation support. Validators receive **display types** (strings for
Principal/bigint), making them ideal for form validation.

```typescript
import { DisplayReactor } from "@ic-reactor/core"

const reactor = new DisplayReactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "backend",
  canisterId,
})

// Register validators that receive display types
reactor.registerValidator("transfer", ([input]) => {
  const issues = []

  if (!input.to) {
    issues.push({ path: ["to"], message: "Recipient is required" })
  }
  if (!/^\d+$/.test(input.amount)) {
    issues.push({ path: ["amount"], message: "Must be a valid number" })
  }

  return issues.length > 0 ? { success: false, issues } : { success: true }
})
```

See [DisplayReactor](https://ic-reactor.b3pay.net/v3/reference/displayreactor#validation) for full validation documentation.

## See Also

- [DisplayReactor](https://ic-reactor.b3pay.net/v3/reference/displayreactor) — Type transformations and validation
- [ClientManager](https://ic-reactor.b3pay.net/v3/reference/clientmanager) — Agent, identity, and cache management
- [createActorHooks](https://ic-reactor.b3pay.net/v3/reference/createactorhooks/overview) — Create React hooks
- [Error Handling](https://ic-reactor.b3pay.net/v3/guides/error-handling) — Handle canister errors
- [Type Safety](https://ic-reactor.b3pay.net/v3/guides/type-safety) — Type system details