Introduction @ic-reactor/react for React Developers

Dear React Developers,

I’m thrilled to introduce @ic-reactor/react, a dedicated React library designed to significantly enhance your development experience on the Internet Computer (IC) blockchain. This library is tailored to harness the full potential of React, providing you with a suite of hooks and utilities for seamless state management, authentication, and type-safe interactions with IC actors.

Key Features:

  • React Hooks Integration: Seamlessly manage blockchain actor states and authentication within your React apps.
  • Type-Safe Actor Interactions: Ensure type safety with your IC actors, thanks to the integration with actor declaration files.
  • Candid Adapter: Fetch Candid interface definitions directly from the network(IC or Local), enabling you to adapt to canisters with changing interfaces or to avoid managing Candid files locally.
  • Auto-Refresh & Query Capabilities: Keep your data fresh with auto-refreshing capabilities and straightforward querying of IC actors.

Getting Started:

To integrate @ic-reactor/react into your project, installation is just a command away:

npm install @ic-reactor/react
# OR
yarn add @ic-reactor/react

Once installed, you have many ways to leverage the power of @ic-reactor/react in your React applications. Let’s explore some of the key features and usage examples to help you get started.

Usage Example:

Dive into @ic-reactor/react with this simple example, showcasing how to create an actor and use the useQueryCall hook for fetching data:

// actor.ts
import { createReactor } from "@ic-reactor/react"
import { backend, canisterId, idlFactory } from "./declarations/backend"

export type Backend = typeof backend

export const { useActorStore, useAuth, useQueryCall } = createReactor<Backend>({
  canisterId,
  idlFactory,
  host: "https://localhost:4943",
})
// Balance.tsx
import { useQueryCall } from "./actor"

const Balance = () => {
  const principal = useUserPrincipal()

  const { call, data, loading, error } = useQueryCall({
    functionName: "get_balance",
    args: [principal],
  })

  return (
    <div>
      <button onClick={call}>{loading ? "Loading..." : "Refresh"}</button>
      {data && <p>Balance: {data}</p>}
      {error && <p>Error: {error.toString()}</p>}
    </div>
  )
}

Authentication Example:

Manage user authentication effortlessly with @ic-reactor/react:

// Login.tsx
import { useAuth } from "./actor"

const Login = () => {
  const { login, logout, identity, authenticated } = useAuth()

  return (
    <div>
      {authenticated ? (
        <>
          <div>Logged in as: {identity.getPrincipal().toText()}</div>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <button onClick={login}>Login</button>
      )}
    </div>
  )
}

Provider-Based Architecture

@ic-reactor/react includes an AgentProvider that establishes a shared IC agent for all your components, ensuring efficient and centralized management of your IC connections. Within this, you can use ActorProvider to specify canisters and manage their state and interactions in a structured manner. This approach is ideal for applications interacting with multiple canisters, as it keeps your code organized and maintainable.

<AgentProvider>
  <Login />
  <ActorProvider
    canisterId="ryjl3-tyaaa-aaaaa-aaaba-cai"
    loadingComponent={<div>Loading Icp Ledger...</div>}
  >
    <ICPBalance />
    <ICPTransfer />
  </ActorProvider>
  <ActorProvider
    canisterId="YOUR_CANISTER_ID"
    loadingComponent={<div>Loading Note Actor...</div>}
  >
    <Notes />
    <AddNote />
  </ActorProvider>
</AgentProvider>

Note: A noteworthy feature of @ic-reactor/react that deserves special mention is its capability to streamline the management of IDL files. Traditionally, interacting with canisters on the Internet Computer requires you to manually manage IDL files (*.did files), which define the interface of your canisters. This can be a cumbersome process, especially in complex projects with multiple canisters.

Then you can use the useAuth, useQueryCall or useUpdateCall and other hooks to interact with the canister.

import { useQueryCall, useUserPrincipal } from "@ic-reactor/react"

export const ICPBalance = () => {
  const principal = useUserPrincipal()

  const { call, data, loading, error } = useQueryCall({
    functionName: "icrc1_balance_of",
    args: [{ owner: principal, subaccount: [] }],
  })

  return (
    <div>
      <h2>ICP Balance:</h2>
      <div>
        Loading: {loading.toString()}
        <br />
        Error: {error?.toString()}
        <br />
        balance:{" "}
        {data !== undefined
          ? JSON.stringify(data, (_, v) =>
              typeof v === "bigint" ? v.toString() : v
            )
          : null}
      </div>
      <button onClick={call}>Get Balance</button>
    </div>
  )
}

Creating Your Own Custom Provider

For developers seeking even more control and customization, @ic-reactor/react enables the creation of your own context providers. This method is type-safe and offers the flexibility to tailor the provider according to your project’s specific needs, allowing for a more customized integration with the IC.

import { createActorContext } from "@ic-reactor/react"
import { backend, canisterId, idlFactory } from "./declarations/backend"

export type Backend = typeof backend

export const {
  ActorProvider: NoteActorProvider,
  useQueryCall: useNoteQueryCall,
  useUpdateCall: useNoteUpdateCall,
} = createActorContext<Backend>({
  canisterId,
  idlFactory,
})

Documentation and Examples:

For a deep dive into all the features and to get your hands on more examples, check out documentation and examples.

Contributing:

We welcome your feedback and contributions to the @ic-reactor project. If you have any questions, suggestions, or issues, feel free to reach out to us on GitHub.


For those who don’t use React, you can still check out this thread @ic-reactor/core on the DFINITY forum. It’s designed to streamline development on the Internet Computer, and may still provide useful tools and insights even if you’re not using React.

8 Likes

This looks cool. I’ll have to look at it in a few days. Remember to also list it here: GitHub - dfinity/awesome-internet-computer: A curated list of awesome projects and resources relating to the Internet Computer Protocol

1 Like

Is it possible to have access to two actors in the same ActorProvider? Maybe I’m not doing this right.

No its not, because each canister should have different canisterId as props on ActorProvider You can use createActorContext and wrap 2 actor on each other and use them in the same component.
Actually I recommend use createActorContext because strong type safety on each method.

const { ActorProvider, useActorState, useQueryCall, useUpdateCall } = createActorContext<typeof backend>({
  canisterId,
  idlFactory, // Optional
});

Edit: when you use the built-in ActorProvider it uses latest context instances, so when using built-in useQueryCall will come from the latest canisterId you passed into the ActorProvider.

1 Like

In the examples doesn’t seem to be a dfx.json file available. I am not sure if you are pulling say the ledger and identity canisters or something else. Thanks!

1 Like

Thanks for the great question! One of the unique features of ic-reactor is the flexibility it offers. You don’t have to manage canisters on your own; instead, ic-reactor leverages the CandidAdapter to fetch the actor interface definition (idlFactory) directly from the canister.

The getCandidDefinition method exemplifies this capability. It first attempts to retrieve the Candid definition from metadata. If that fails, it falls back to a temporary hack method. Should both attempts fail, an error is thrown.

Additionally, ic-reactor provides a convenient didTojs method, which translates the Candid file to a JavaScript module using the didtojs canister. This functionality is akin to what the candid-ui employs for interacting with canisters. Kudos to @chenyan for the inspiration!

With ic-reactor, developers can enjoy a seamless development experience, whether they’re building with local canisters or existing canisters already deployed on the network. This eliminates the need to worry about pulling or handling canister candid declarations, streamlining the development process.

Feel free to reach out if you have any further questions or need assistance with ic-reactor. Happy coding!

2 Likes

:rocket: New Example: Using Multiple Actors in a Single Component! :rocket:

I’m thrilled to share a new example I’ve added to ic-reactor that demonstrates a powerful capability: using multiple actors in a single React component. This feature unlocks a world of possibilities for building complex decentralized applications on the Internet Computer blockchain.

In this example, we have a React component called Donation, which integrates two actors seamlessly: the ICP Ledger Canister and the ICDV Donation Canister. Let’s dive into the details:

//...other staff
  const {
    call: refetchBalance,
    data: balance,
    loading: balanceLoading,
  } = useICRC2QueryCall({
    functionName: "icrc1_balance_of",
    args: [{ owner: principal, subaccount: [] }],
  })

  const {
    call: refetchAllowance,
    data: allowance,
    loading: allowanceLoading,
  } = useICRC2QueryCall({
    functionName: "icrc2_allowance",
    args: [
      {
        account: { owner: principal, subaccount: [] },
        spender: {
          owner: icdvCanisterId,
          subaccount: [],
        },
      },
    ],
  })

  const {
    call: mintFromICP,
    loading: mintFromICPLoading,
    data: mintFromICPResult,
    error: mintFromICPError,
  } = useICDVUpdateCall({
    functionName: "mintFromICP",
    onSuccess: () => {
      refetchBalance()
    },
  })

  const {
    call: approve,
    loading: approveLoading,
    data: approveResult,
  } = useICRC2UpdateCall({
    functionName: "icrc2_approve",
    onSuccess: () => {
      refetchAllowance()
      mintFromICP([
        {
          amount: BigInt(amountRef.current?.value || "0"),
          source_subaccount: [],
          target: [],
        },
      ])
    },
  })

// render data and form

How It Works:

  1. Initialization: The Donation component initializes and retrieves the necessary information from both canisters using hooks provided by ic-reactor context api.
  2. Balance and Allowance: It fetches and displays the user’s balance from the ICP Ledger Canister and the allowance granted to the ICDV Donation Canister.
  3. Donation Form: Users can input the amount they want to donate and submit the form.
  4. Transaction Flow: Upon donation submission, the component orchestrates the transaction flow seamlessly. Here’s where the magic of chaining comes into play:
  • When the user initiates a donation, it first triggers the approve function on icp ledger.
  • Upon successful allowance, it automatically calls the mintFromICP function from icdv canister to mints tokens.
  1. Dynamic UI: The UI dynamically updates to reflect the transaction status, providing real-time feedback to the user.

Happy coding! :rocket:

4 Likes

I am no expert but this looks really amazing for simplifying frontend.

question:
Starting a new react project with dfx would do something like. Create a new project install ic-reactor and delete dfx.json? Or not sure of the starter setup. Could you clarify. Thanks!

1 Like

Nice work! Could you post a tweet about this? Would like to promote this update.

1 Like

It work with dfx and declarations files even easier and faster, there is not force to delete anything just install ic-reactor and use it!
Ic-reactor uses @dfinity/agent behind the scene so you don’t have to delete that too!
You can do whatever you want, it work seamlessly with local network(II, canisters), please take a look at this starter or this template you will get the idea.

1 Like

Thank you, Actually this feature was available from the beginning, should I make another video for react?

2 Likes

Thanks! I am getting there. This is great!

I was wondering if there is a way to have a local testing identity?

update:
So I pulled the II canister and used this : “http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:4943”.
instead of:

login({
    identityProvider: "https://identity.ic0.app",
})

Seems to work great.

2 Likes

I’m glad you’re getting started with the library and you are absolutely right.
This function work with the @dfinity/auth-client login function behind the scene and this is the parameters doc.

2 Likes

Having a bit of a hard time with the ledger canister. What is the process for getting that working locally? Not sure what /declarations/icp-ledger.ts does. I have the ledger canister deployed locally just not sure how best to interact with it. Can I use useQueryCall if ActorProvider has canisterId="local_ledger_canister_id"?

This is the setup you should use:

<AgentProvider withLocalEnv>{/*  or `withProcessEnv` that will use DFX_NETWORK .env file */}
  <ActorProvider canisterId="ryjl3-tyaaa-aaaaa-aaaba-cai">{/* replace it with your actual canisterId, also you can use `import { canisterId } from "/declarations/icp-ledger"` */}
    <ICPBalance />
  </ActorProvider>
</AgentProvider>

and then in the ICPBalance component you can use useQueryCall or useUpdateCall:

import { useQueryCall, useUserPrincipal } from "@ic-reactor/react"
import { ICPLedger } from "./declarations/icp-ledger"
import { Principal } from "@ic-reactor/react/dist/types"

export const ICPBalance = () => {
  const principal = useUserPrincipal() as Principal

  const { call, data, loading, error } = useQueryCall<
    ICPLedger,
    "icrc1_balance_of"
  >({
    functionName: "icrc1_balance_of",
    args: [{ owner: principal, subaccount: [] }],
  })

  return (
    <div>
      <h2>ICP Balance:</h2>
      <div>
        Loading: {loading.toString()}
        <br />
        Error: {error?.toString()}
        <br />
        balance:{" "}
        {data !== undefined
          ? JSON.stringify(data, (_, v) =>
              typeof v === "bigint" ? v.toString() : v
            )
          : null}
      </div>
      <button onClick={call}>Get Balance</button>
    </div>
  )
}

My recommended approach for large-scale applications is as follows:

  1. Set up ActorContext for each actor:
import { createActorContext } from "@ic-reactor/react"
import { ledger, canisterId, idlFactory } from "declarations/ledger"

export type Ledger = typeof ledger

export const {
  ActorProvider: LedgerActorProvider,
  useQueryCall: useLedgerQueryCall,
  useUpdateCall: useLedgerUpdateCall,
} = createActorContext<Ledger>({
  canisterId,
  idlFactory,
})
  1. wrap the application with your ActorProvider and use the hooks whenever needed.
// In your root component or a designated provider component
import { AgentProvider } from "@ic-reactor/react";
import { LedgerActorProvider } from "./path/to/actorContext";

function App() {
  return (
    <AgentProvider withProcessEnv>
      <LedgerActorProvider>
        {/* Your application components here */}
      </LedgerActorProvider>
    </AgentProvider>
  );
}

This setup is typesafe and straightforward, allowing you to use multiple hooks inside one component seamlessly!

1 Like