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
yarn add @aave/react@latest
pnpm add @aave/react@latest
2. Set Up the Client
Then, create an Aave instance that will be used to interact with the protocol.
import { AaveClient } from "@aave/react";
export const client = AaveClient.create();
@aave/ already includes and re-exports @aave/, so you don’t
need to install it.
3. Set Up the Provider
Next, wrap your app with the <Aave component and pass the client instance.
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.
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
yarn add viem@2
pnpm add viem@2
Ensure you have ethers package installed in your project.
npm install ethers
yarn add ethers
pnpm add ethers
Ensure you have viem and @privy-io/ packages installed in your project.
npm install viem@2 @privy-io/react-auth
yarn add viem@2 @privy-io/react-auth
pnpm add viem@2 @privy-io/react-auth
@privy-io/ requires the viem package.
Ensure you have @dynamic-labs/ and @dynamic-labs/ packages installed in your project.
npm install @dynamic-labs/sdk-react-core @dynamic-labs/ethereum
yarn add @dynamic-labs/sdk-react-core @dynamic-labs/ethereum
pnpm add @dynamic-labs/sdk-react-core @dynamic-labs/ethereum
Ensure you have thirdweb package installed in your project.
npm install thirdweb@5
yarn add thirdweb@5
pnpm add thirdweb@5
Ensure you have viem, @turnkey/ and @turnkey/ packages installed in your project.
npm install viem@2 @turnkey/viem @turnkey/react-wallet-kit
yarn add viem@2 @turnkey/viem @turnkey/react-wallet-kit
pnpm add viem@2 @turnkey/viem @turnkey/react-wallet-kit
Send Aave Transactions
To send transactions through the connected wallet, use the use imperative hook.
const [sendTransaction, { loading, error }] = useSendTransaction(/* args */);
This hook abstracts the wallet interaction and provides an interface for submitting transactions.
Import the use hook from the @aave/ entry point and wire it up with the viem’s Wallet.
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 Wallet from the
connected wallet.
Import the use hook from the @aave/ entry point and wire it up with the ethers’s Signer.
import { useSendTransaction } from "@aave/react/ethers";
import { ethers } from "ethers";
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
function MyComponent() {
const [sendTransaction, sending] = useSendTransaction(signer);
// Use with transaction hooks…
}
Import the use hook from the @aave/ entry point.
import { useSendTransaction } from "@aave/react/privy";
function MyComponent() {
const [sendTransaction, sending] = useSendTransaction();
// Use with transaction hooks…
}
Import the use hook from the @aave/ entry point and pass the Dynamic wallet client.
import { useSendTransaction } from "@aave/react/viem";
import { isEthereumWallet } from "@dynamic-labs/ethereum";
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { useEffect, useState } from "react";
import { WalletClient } from "viem";
function MyComponent() {
const { primaryWallet } = useDynamicContext();
const [walletClient, setWalletClient] = useState<WalletClient | null>(null);
const [sendTransaction, sending] = useSendTransaction(walletClient);
useEffect(() => {
if (primaryWallet && isEthereumWallet(primaryWallet)) {
primaryWallet.getWalletClient().then(setWalletClient);
}
}, [primaryWallet]);
// Use with transaction hooks…
}
Import the use hook from the @aave/ entry point and configure it with your thirdweb client.
import { useSendTransaction } from "@aave/react/thirdweb";
import { createThirdwebClient } from "thirdweb";
const thirdwebClient = createThirdwebClient({
clientId: "<YOUR_THIRDWEB_CLIENT_ID>",
});
function MyComponent() {
const [sendTransaction, sending] = useSendTransaction(thirdwebClient);
// Use with transaction hooks…
}
Import the use hook from the @aave/ entry point and wire it up with a viem Wallet created from Turnkey’s account.
import { useSendTransaction } from "@aave/react/viem";
import { createAccount } from "@turnkey/viem";
import { useTurnkey } from "@turnkey/react-wallet-kit";
import { useEffect, useState } from "react";
import { createWalletClient, http, type WalletClient } from "viem";
import { mainnet } from "viem/chains";
function MyComponent() {
const { httpClient, wallets, fetchWalletAccounts } = useTurnkey();
const [walletClient, setWalletClient] = useState<WalletClient | null>(null);
const [sendTransaction, sending] = useSendTransaction(walletClient);
useEffect(() => {
if (!httpClient || !wallets.length) return;
fetchWalletAccounts({ wallet: wallets[0] }).then(async ([account]) => {
const tkAccount = await createAccount({
client: httpClient,
organizationId: account.organizationId,
signWith: account.address,
});
setWalletClient(
createWalletClient({
account: tkAccount,
chain: mainnet,
transport: http(),
})
);
});
}, [httpClient, wallets, fetchWalletAccounts]);
// Use with transaction hooks…
}
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 use hook to sign ERC-20 permits.
Import the use hook from the @aave/ entry point and wire it up with the viem’s Wallet.
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 Wallet from the
connected wallet.
Import the use hook from the @aave/ entry point and wire it up with the ethers’s Signer.
import { useERC20Permit } from "@aave/react/ethers";
import { ethers } from "ethers";
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
function MyComponent() {
const [signPermit, sending] = useERC20Permit(signer);
// Use with transaction hooks…
}
Import the use hook from the @aave/ entry point.
import { useERC20Permit } from "@aave/react/privy";
function MyComponent() {
const [signPermit, sending] = useERC20Permit();
// Use with transaction hooks…
}
Import the use hook from the @aave/ entry point and pass the Dynamic wallet client.
import { useERC20Permit } from "@aave/react/viem";
import { isEthereumWallet } from "@dynamic-labs/ethereum";
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { useEffect, useState } from "react";
import { WalletClient } from "viem";
function MyComponent() {
const { primaryWallet } = useDynamicContext();
const [walletClient, setWalletClient] = useState<WalletClient | null>(null);
const [signPermit, sending] = useERC20Permit(walletClient);
useEffect(() => {
if (primaryWallet && isEthereumWallet(primaryWallet)) {
primaryWallet.getWalletClient().then(setWalletClient);
}
}, [primaryWallet]);
// Use with transaction hooks…
}
Import the use hook from the @aave/ entry point.
import { useERC20Permit } from "@aave/react/thirdweb";
function MyComponent() {
const [signPermit, sending] = useERC20Permit();
// Use with transaction hooks…
}
Import the use hook from the @aave/ entry point and wire it up with a viem Wallet created from Turnkey’s account.
import { useERC20Permit } from "@aave/react/viem";
import { createAccount } from "@turnkey/viem";
import { useTurnkey } from "@turnkey/react-wallet-kit";
import { useEffect, useState } from "react";
import { createWalletClient, http, type WalletClient } from "viem";
import { mainnet } from "viem/chains";
function MyComponent() {
const { httpClient, wallets, fetchWalletAccounts } = useTurnkey();
const [walletClient, setWalletClient] = useState<WalletClient | null>(null);
const [signPermit, sending] = useERC20Permit(walletClient);
useEffect(() => {
if (!httpClient || !wallets.length) return;
fetchWalletAccounts({ wallet: wallets[0] }).then(async ([account]) => {
const tkAccount = await createAccount({
client: httpClient,
organizationId: account.organizationId,
signWith: account.address,
});
setWalletClient(
createWalletClient({
account: tkAccount,
chain: mainnet,
transport: http(),
})
);
});
}, [httpClient, wallets, fetchWalletAccounts]);
// Use with transaction hooks…
}
Then, use the sign 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:
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.
// 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.
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.
const [execute, state] = useActionHook();
Execute Function
The execute function returns a Result, a thenable object that represents one of two states:
Ok<T>: A successful result containing a value of typeTErr<E>: A failure containing an error of typeE
Awaiting a Result resolves to a Result<T, E>.
With a Result<T, E>, you can use the convenient is and is methods to check the outcome and narrow the type.
If you have a Result, you must first await it to access those methods.
const result = await execute();
if (result.isOk()) {
console.log("Result:", result.value);
} else {
console.error("Error:", result.error);
}
This approach avoids reliance on try/ 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:
const { called, data, error, loading } = state;
Where:
called:truewhen the operation has been executed at least once,falseotherwisedata: Contains the last successful result of typeT,undefinedotherwiseerror: Contains the error of typeEif the operation failed,undefinedotherwiseloading:truewhen the operation is in progress,falseotherwise
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 use hook according to the wallet provider you are using.
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.
const result = await prepare(/* args */).andThen(sendTransaction);
Optionally, combine the state objects to drive your UI components.
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);
}
function MyComponent() {
// …
const loading = preparing.loading || sending.loading;
const error = preparing.error || sending.error;
return (
<div>
<button disabled={loading}>
{loading ? "Sending…" : "Send Transaction"}
</button>
{error && <p style={{ color: "red" }}>{error.message}</p>}
</div>
);
}
Complex Transactions
To handle a complex transaction hook, follow the steps below.
1. Use Complex Transaction Hooks
First, instantiate the transaction hook and the use hook according to the wallet provider you are using.
const [prepare, preparing] = useComplexTransaction();
const [sendTransaction, sending] = useSendTransaction(/* … */);
Optionally, combine the state objects to drive your UI components.
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.
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);
}
function MyComponent() {
// …
const loading = preparing.loading || sending.loading;
const error = preparing.error || sending.error;
return (
<div>
<button disabled={loading}>
{loading ? "Sending…" : "Send Transaction"}
</button>
{error && <p style={{ color: "red" }}>{error.message}</p>}
</div>
);
}