ic-use-actor
TypeScript icon, indicating that this package has built-in type declarations

0.2.0 • Public • Published

ic-use-actor

A React hook library for interacting with Internet Computer (IC) canisters. ic-use-actor provides a simple, type-safe way to interact with IC actors using XState stores for state management.

version downloads

Features

  • Simple API: Just one function call to create a typed hook for your canister
  • No Provider Hell: No need for React Context or Provider components
  • Type Safety: Full TypeScript support with canister service definitions
  • Request/Response Interceptors: Process requests and responses with customizable callbacks
  • Global State Management: Powered by XState stores for predictable state management
  • Multiple Canisters: Easy to work with multiple canisters without nesting providers

Table of Contents

Installation

npm install ic-use-actor @dfinity/agent @dfinity/candid @xstate/store

or

yarn add ic-use-actor @dfinity/agent @dfinity/candid @xstate/store

or

pnpm add ic-use-actor @dfinity/agent @dfinity/candid @xstate/store

Quick Start

// 1. Create your actor hook
import { createActorHook } from "ic-use-actor";
import { canisterId, idlFactory } from "./declarations/my_canister";
import { _SERVICE } from "./declarations/my_canister/my_canister.did";

export const useMyCanister = createActorHook<_SERVICE>({
  canisterId,
  idlFactory,
});

// 2. Use it in your components
function MyComponent() {
  const { actor: myCanister, authenticate, isAuthenticated, isInitializing, error } = useMyCanister();
  const { identity, clear } = useInternetIdentity(); // or any identity provider

  useEffect(() => {
    if (identity) {
      authenticate(identity);
    }
  }, [identity, authenticate]);

  const handleClick = async () => {
    if (!myCanister) return;
    const result = await myCanister.myMethod();
    console.log(result);
  };

  if (error) return <div>Error: {error.message}</div>;
  if (isInitializing) return <div>Loading...</div>;
  if (!isAuthenticated) return <div>Please sign in</div>;

  return <button onClick={handleClick}>Call Canister</button>;
}

// 3. That's it! No providers needed in your App
function App() {
  return <MyComponent />;
}

Usage

Basic Setup

Create a hook for your canister by calling createActorHook with your canister's configuration:

// actors.ts
import { createActorHook } from "ic-use-actor";
import { canisterId, idlFactory } from "./declarations/backend";
import { _SERVICE } from "./declarations/backend/backend.did";

export const useBackendActor = createActorHook<_SERVICE>({
  canisterId,
  idlFactory,
});

Using in Components

The hook returns an object with the actor instance and several utility functions:

function MyComponent() {
  const {
    actor,           // The actor instance (initialized with anonymous agent by default)
    authenticate,    // Function to authenticate the actor with an identity
    setInterceptors, // Function to set up interceptors
    isAuthenticated, // Boolean indicating if actor is authenticated
    isInitializing,  // Boolean indicating if actor is being initialized
    error,           // Any error that occurred during initialization
    reset,           // Function to reset the actor state
    clearError       // Function to clear error state
  } = useBackendActor();

  const { identity, clear } = useInternetIdentity();

  // Authenticate when identity is available
  useEffect(() => {
    if (identity) {
      authenticate(identity);
    }
  }, [identity, authenticate]);

  // Use the actor (works with anonymous or authenticated)
  const fetchData = async () => {
    if (!actor) return;
    try {
      const data = await actor.getData();
      console.log(data);
    } catch (err) {
      console.error("Failed to fetch data:", err);
    }
  };

  return (
    <div>
      {error && <div>Error: {error.message}</div>}
      {isInitializing && <div>Initializing...</div>}
      <button onClick={fetchData} disabled={!actor}>Fetch Data</button>
      {isAuthenticated && <span>Authenticated</span>}
    </div>
  );
}

Multiple Canisters

Working with multiple canisters is straightforward - just create a hook for each:

// actors.ts
export const useBackendActor = createActorHook<BackendService>({
  canisterId: backendCanisterId,
  idlFactory: backendIdlFactory,
});

export const useNFTActor = createActorHook<NFTService>({
  canisterId: nftCanisterId,
  idlFactory: nftIdlFactory,
});

export const useTokenActor = createActorHook<TokenService>({
  canisterId: tokenCanisterId,
  idlFactory: tokenIdlFactory,
});

// Component using multiple actors
function MultiCanisterComponent() {
  const { identity } = useSiweIdentity();
  const backend = useBackendActor();
  const nft = useNFTActor();
  const token = useTokenActor();

  useEffect(() => {
    if (identity) {
      backend.authenticate(identity);
      nft.authenticate(identity);
      token.authenticate(identity);
    }
  }, [identity]);

  // Use the actors...
}

Advanced Usage

Interceptors

Add request/response interceptors to proxy and process or log interactions with your canister. Interceptors intercept booth outgoing requests and incoming responses as well as errors.

function MyComponent() {
  const { actor, authenticate, setInterceptors } = useBackendActor();
  const { identity, logout } = useAuthProvider();
  const navigate = useNavigate();

  // Set up interceptors once - they can access React hooks
  useEffect(() => {
    setInterceptors({
      // Called before each request
      onRequest: (data) => {
        console.log(`Calling ${data.methodName}`, data.args);
        // Modify args if needed
        return data.args;
      },

      // Called after successful responses
      onResponse: (data) => {
        console.log(`Response from ${data.methodName}`, data.response);
        // Modify response if needed
        return data.response;
      },

      // Called on request errors (e.g., network issues)
      onRequestError: (data) => {
        console.error(`Request error in ${data.methodName}`, data.error);
        // Transform or handle error
        return data.error;
      },

      // Called on response errors - can access React hooks here!
      onResponseError: (data) => {
        console.error(`Response error in ${data.methodName}`, data.error);

        // Check for expired identity and handle it
        if (data.error.message?.includes("delegation expired")) {
          logout(); // Call React hook function
          navigate('/login'); // Use React Router
        }

        return data.error;
      },
    });
  }, [setInterceptors, logout, navigate]);

  // Authenticate when identity is available
  useEffect(() => {
    if (identity) {
      authenticate(identity);
    }
  }, [identity, authenticate]);

  // ... rest of component
}

Error Handling

The hook provides error state that you can use to handle initialization errors:

function MyComponent() {
  const { actor, error, clearError, authenticate } = useBackendActor();
  const { identity } = useSiweIdentity();

  if (error) {
    return (
      <div>
        <p>Error: {error.message}</p>
        <button onClick={() => {
          clearError();
          if (identity) {
            authenticate(identity);
          }
        }}>
          Retry
        </button>
      </div>
    );
  }

  // ...
}

Custom HTTP Agent Options

Configure the HTTP agent with custom options:

export const useBackendActor = createActorHook<_SERVICE>({
  canisterId,
  idlFactory,
  httpAgentOptions: {
    host: "https://ic0.app",
    credentials: "include",
    headers: {
      "X-Custom-Header": "value",
    },
  },
  actorOptions: {
    callTransform: (methodName, args, callConfig) => {
      // Transform calls before sending
      return [methodName, args, callConfig];
    },
    queryTransform: (methodName, args, callConfig) => {
      // Transform queries before sending
      return [methodName, args, callConfig];
    },
  },
});

API Reference

createActorHook

Creates a React hook for interacting with an IC canister.

function createActorHook<T>(options: CreateActorHookOptions<T>): () => UseActorReturn<T>

Options

Option Type Required Description
canisterId string Yes The canister ID
idlFactory IDL.InterfaceFactory Yes The IDL factory for the canister
httpAgentOptions HttpAgentOptions No Options for the HTTP agent
actorOptions ActorConfig No Options for the actor

Hook Return Value

createActorHook returns a hook that provides:

interface UseActorReturn<T> {
  actor: ActorSubclass<T> | undefined;
  isInitializing: boolean;
  isAuthenticated: boolean;
  error: Error | undefined;
  authenticate: (identity: Identity) => Promise<void>;
  setInterceptors: (interceptors: InterceptorOptions) => void;
  reset: () => void;
  clearError: () => void;
}
Property Type Description
actor ActorSubclass<T> | undefined The actor instance (initialized with anonymous agent by default)
isInitializing boolean Whether the actor is being initialized
isAuthenticated boolean Whether the actor is authenticated with a non-anonymous identity
error Error | undefined Any error that occurred during initialization or authentication
authenticate (identity: Identity) => Promise<void> Function to authenticate the actor with an identity
setInterceptors (interceptors: InterceptorOptions) => void Function to set up interceptors
reset () => void Function to reset the actor state
clearError () => void Function to clear error state

Migration from v0.1.x

If you're upgrading from v0.1.x, check out the Migration Guide for detailed instructions on updating your code to use the new API.

Author

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT

Package Sidebar

Install

npm i ic-use-actor

Weekly Downloads

17

Version

0.2.0

License

MIT

Unpacked Size

64.7 kB

Total Files

12

Last publish

Collaborators

  • kristoferlund