Skip to Content
docs

Aave V3 React Quickstart

AaveKit React is a collection of React hooks for building decentralized applications on top of the Aave Protocol. It provides a type-safe way to interact with Aave markets, manage user positions, and execute transactions.

Getting Started

1. Install SDK

First, install the AaveKit React packages using your package manager of choice.

npm install @aave/react@latest

2. Set Up the Client

Then, create an AaveClient instance that will be used to interact with the protocol.

client.ts
import { AaveClient } from "@aave/react";

export const client = AaveClient.create();

@aave/react already includes and re-exports @aave/client, so you don’t need to install it.

3. Set Up the Provider

Next, wrap your app with the <AaveProvider> component and pass the client instance.

App.tsx
import { AaveProvider } from "@aave/react";

import { client } from "./client";

export function App() {
  return (
    <AaveProvider client={client}>
      {/* Your application components */}
    </AaveProvider>
  );
}

4. Start Building

You can now start using AaveKit React.

Example
import { useAaveChains } from "@aave/react";

export function SupportedChains() {
  const { data: chains } = useAaveChains({});

  return (
    <div>
      <h2>Supported Chains</h2>
      {chains?.map((chain) => (
        <div key={chain.chainId}>
          <h3>{chain.name}</h3>
          <p>Chain ID: {chain.chainId}</p>
        </div>
      ))}
    </div>
  );
}

Integrations

AaveKit React includes first-class support for viem, ethers v6, Privy, Dynamic, thirdweb and Turnkey.

Ensure you have viem package installed in your project.

npm install viem@2

Send Aave Transactions

To send transactions through the connected wallet, use the useSendTransaction imperative hook.

TSX
const [sendTransaction, { loading, error }] = useSendTransaction(/* args */);

This hook abstracts the wallet interaction and provides an interface for submitting transactions.

Import the useSendTransaction hook from the @aave/react/viem entry point and wire it up with the viem’s WalletClient.

Viem
import { useSendTransaction } from "@aave/react/viem";
import { useWalletClient } from "wagmi";

function MyComponent() {
  const { data: walletClient } = useWalletClient();
  const [sendTransaction, sending] = useSendTransaction(walletClient);

  // Use with transaction hooks…
}

The example uses wagmi to get the WalletClient from the connected wallet.

Then, use it as described in each operation guide.

Sign ERC-20 Permits

Aave makes use of EIP-2612 to allow users to avoid ERC-20 approval before sending transactions.

Use the useERC20Permit hook to sign ERC-20 permits.

Import the useERC20Permit hook from the @aave/react/viem entry point and wire it up with the viem’s WalletClient.

Viem
import { useERC20Permit } from "@aave/react/viem";
import { useWalletClient } from "wagmi";

function MyComponent() {
  const { data: walletClient } = useWalletClient();
  const [signPermit, sending] = useERC20Permit(walletClient);

  // Use with transaction hooks…
}

The example uses wagmi to get the WalletClient from the connected wallet.

Then, use the signPermit function as described in the specific operation guide.

Declarative Hooks

All declarative hooks (market data, user positions, etc.) support two ways of operating: loading state and React Suspense.

Loading State

Handle loading state manually in your component:

Loading States
function ChainsList() {
  const { data, loading, error } = useAaveChains({});

  if (loading) return <div>Loading…</div>;

  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {data.map((chain) => (
        <div key={chain.chainId}>{chain.name}</div>
      ))}
    </div>
  );
}

React Suspense

Let React handle loading states automatically through a Suspense boundary.

With Suspense
// Component - no loading states needed
function ChainsList() {
  const { data: chains } = useAaveChains({
    suspense: true, // Enable suspense mode
  });

  return (
    <div>
      {chains.map((chain) => (
        <div key={chain.chainId}>{chain.name}</div>
      ))}
    </div>
  );
}

// Parent - wrap with Suspense boundary
function ChainsPage() {
  return (
    <Suspense fallback={<div>Loading …</div>}>
      <ChainsList />
    </Suspense>
  );
}

You can handle errors in the parent component by wrapping the component with an error boundary component.

With Error Boundary
import { ErrorBoundary } from "react-error-boundary";

// …

function ChainsPage() {
  return (
    <ErrorBoundary fallback={<p>Something went wrong</p>}>
      <Suspense fallback={<div>Loading …</div>}>
        <ChainsList />
      </Suspense>
    </ErrorBoundary>
  );
}

Imperative Hooks

AaveKit React provides imperative hooks that are designed to be triggered manually when you need to perform an operation. Imperative hooks return an execute function and a state object.

TSX
const [execute, state] = useActionHook();

Execute Function

The execute function returns a ResultAsync<T, E>, a thenable object that represents one of two states:

  • Ok<T>: A successful result containing a value of type T
  • Err<E>: A failure containing an error of type E

Awaiting a ResultAsync<T, E> resolves to a Result<T, E>.

With a Result<T, E>, you can use the convenient isOk() and isErr() methods to check the outcome and narrow the type.
If you have a ResultAsync<T, E>, you must first await it to access those methods.

TypeScript
const result = await execute();

if (result.isOk()) {
  console.log("Result:", result.value);
} else {
  console.error("Error:", result.error);
}

This approach avoids reliance on try/catch blocks and promotes predictable, type-safe code by ensuring errors are handled explicitly.

AaveKit re-exports the NeverThrow library. See its docs for details.

State Object

The state object returned by imperative hooks provides information about the current operation status:

TypeScript
const { called, data, error, loading } = state;

Where:

  • called: true when the operation has been executed at least once, false otherwise
  • data: Contains the last successful result of type T, undefined otherwise
  • error: Contains the error of type E if the operation failed, undefined otherwise
  • loading: true when the operation is in progress, false otherwise

Transaction Hooks

Transaction hooks are a specific type of imperative hook that handle protocol interactions like supply, borrow, withdraw, repay, and more.

There are two types of transaction hooks:

  • Simple transactions - single transactions that can be sent directly to the wallet
  • Complex transactions - transactions that could require approval beforehand

Simple Transactions

To handle a simple transaction hook, follow the steps below.

1. Use Simple Transaction Hooks

First, instantiate the transaction hook and the useSendTransaction hook according to the wallet provider you are using.

Instantiate Hooks
const [prepare, preparing] = useSimpleTransaction();
const [sendTransaction, sending] = useSendTransaction(/* … */);

2. Execute the Transaction

Next, execute the transaction in your app by chaining the prepare and send operations.

TypeScript
const result = await prepare(/* args */).andThen(sendTransaction);

Optionally, combine the state objects to drive your UI components.

TypeScript
const loading = preparing.loading || sending.loading;
const error = preparing.error || sending.error;

3. Handle the Simple Transaction Result

Finally, handle the result of the operation.

if (result.isErr()) {
  switch (result.error.name) {
    case "SigningError":
      // Most likely the user rejected the transaction
      console.error(`Failed to sign the transaction: ${result.error.message}`);
      break;

    case "TimeoutError":
      console.error(`Transaction timed out: ${result.error.message}`);
      break;

    case "TransactionError":
      console.error(`Transaction failed: ${result.error.message}`);
      break;

    case "UnexpectedError":
      console.error(result.error.message);
      break;
  }
} else {
  console.log("Transaction sent with hash:", result.value);
}

Complex Transactions

To handle a complex transaction hook, follow the steps below.

1. Use Complex Transaction Hooks

First, instantiate the transaction hook and the useSendTransaction hook according to the wallet provider you are using.

Instantiate Hooks
const [prepare, preparing] = useComplexTransaction();
const [sendTransaction, sending] = useSendTransaction(/* … */);

Optionally, combine the state objects to drive your UI components.

TypeScript
const loading = preparing.loading || sending.loading;
const error = preparing.error || sending.error;

2. Process the Execution Plan

Next, handle the execution plan in your transaction flow. Some operations may require token approval before executing the main transaction.

TypeScript
import { errAsync } from "@aave/react";

const result = await prepare(/* args */).andThen((plan) => {
  switch (plan.__typename) {
    case "TransactionRequest":
      return sendTransaction(plan);

    case "ApprovalRequired":
      return sendTransaction(plan.approval).andThen(() =>
        sendTransaction(plan.originalTransaction)
      );

    case "InsufficientBalanceError":
      return errAsync(
        new Error(`Insufficient balance: ${plan.required.value} required.`)
      );
  }
});

// …

To ask the user for confirmation before sending, add your app’s confirmation UI here.

3. Handle the Complex Transaction Result

Finally, handle the result of the operation.

if (result.isErr()) {
  switch (result.error.name) {
    case "SigningError":
      // Most likely the user rejected the transaction
      console.error(`Failed to sign the transaction: ${result.error.message}`);
      break;

    case "TimeoutError":
      console.error(`Transaction timed out: ${result.error.message}`);
      break;

    case "TransactionError":
      console.error(`Transaction failed: ${result.error.message}`);
      break;

    case "UnexpectedError":
      console.error(result.error.message);
      break;
  }
} else {
  console.log("Transaction sent with hash:", result.value);
}

Next Steps