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.
- 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
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
// 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 />;
}
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,
});
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>
);
}
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...
}
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
}
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>
);
}
// ...
}
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];
},
},
});
Creates a React hook for interacting with an IC canister.
function createActorHook<T>(options: CreateActorHookOptions<T>): () => UseActorReturn<T>
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 |
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 |
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.
- kristofer@fmckl.se
- Twitter: @kristoferlund
- Discord: kristoferkristofer
- Telegram: @kristoferkristofer
Contributions are welcome! Please feel free to submit a Pull Request.
MIT