Aave V4 React Quickstart
AaveKit React for Aave V4 is a collection of React hooks for building decentralized applications on top of Aave V4.
Getting Started
To get started, follow the steps below.
1. Install SDK
First, install the latest AaveKit React packages using your package manager of choice.
npm install @aave/react@next
yarn add @aave/react@next
pnpm add @aave/react@next
2. Set Up 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 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. Here’s a basic example to fetch supported chains:
import { useChains } from "@aave/react";
export function SupportedChains() {
const { data: chains } = useChains();
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>
);
}
Declarative Hooks
AaveKit React declarative hooks execute on render with the given inputs, refresh when inputs change, and update after successful write operations; declarative hooks support two modes: loading state and React Suspense.
Handle loading state manually in your component:
import { useChains } from "@aave/react";
function ChainsList() {
const { data, loading, error } = useChains();
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>
);
}
Let React handle loading states automatically through a Suspense boundary by wrapping the component with a React <Suspense> component higher in the component tree.
import { useChains } from "@aave/react";
function ChainsList() {
const { data } = useChains({
suspense: true, // Enable suspense mode
});
return (
<div>
{data.map((chain) => (
<div key={chain.chainId}>{chain.name}</div>
))}
</div>
);
}
import { Suspense } from "react";
function ChainsPage() {
return (
<Suspense fallback={<div>Loading…</div>}>
<ChainsList />
</Suspense>
);
}
You can also handle errors in a parent component by wrapping the component with an 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>
);
}
Pausable Reads
AaveKit React read hooks support pausing, which defers data fetching until certain conditions are met. This is useful when:
- Waiting for user input (e.g., wallet connection, user address)
- Conditional data loading (e.g., only fetch when a user is authenticated)
- Dependent data (e.g., fetch user data only after chain selection)
Type safety is maintained by allowing nullish request parameters and returning a paused state, which explicitly signals when a hook is inactive and lets TypeScript precisely narrow types between paused and active states.
import { useChains } from "@aave/react";
const { data, loading, error, paused } = useChains({
pause: !connected,
});
// …
// data: Chain[] | undefined
// loading: boolean
// error: UnexpectedError | undefined
// paused: boolean
if (paused) {
// data: undefined
// error: undefined
// loading: boolean
return null;
}
if (loading) {
// data: undefined
// error: undefined
// loading: true
return null;
}
if (error) {
// data: undefined
// error: UnexpectedError
// loading: false
return null;
}
// data: Chain[] (guaranteed to be defined)
import { useChains } from "@aave/react";
const { data, paused } = useChains({
pause: !connected,
suspense: true,
});
// …
// data: Chain[] | undefined
// paused: boolean
if (paused) {
// data: undefined
return null;
}
// data: Chain[] (guaranteed to be defined)
Reloading State
Declarative hooks also expose a reloading state that indicates when data is being refreshed after the initial load. This is different from loading, which is only true during the first fetch.
The reloading state becomes true when:
- Variables change and the hook refetches with new parameters
- Polling triggers a background refresh (if the hook is designed to poll)
This lets you show a subtle refresh indicator while keeping the previous data visible, rather than replacing content with a loading spinner.
import { useUserPositions } from "@aave/react";
const { data, loading, error, reloading } = useUserPositions({
user: userAddress,
});
if (loading) {
return <Spinner />;
}
if (error) {
return <ErrorMessage error={error} />;
}
// Show refresh indicator while keeping data visible
return (
<div>
{reloading && <RefreshIndicator />}
<PositionsList positions={data} />
</div>
);
import { useUserPositions } from "@aave/react";
const { data, reloading } = useUserPositions({
user: userAddress,
suspense: true,
});
// Show refresh indicator while keeping data visible
return (
<div>
{reloading && <RefreshIndicator />}
<PositionsList positions={data} />
</div>
);
Imperative Hooks
AaveKit React imperative hooks return an execute function and a state object. These are tailored for when you need explicit control, like event handlers or multi‑step flows.
const [execute, state] = useImperativeHook();
Read hooks with this pattern don’t watch for updates. Use the imperative variant for on-demand fresh data, such as in an event handler.
Execute Function
The execute function takes the hook arguments and, when awaited, yields a Result<T, E>.
Use is and is to check the outcome and narrow the type:
const result = await execute();
if (result.isOk()) {
console.log("Result:", result.value);
} else {
console.error("Error:", result.error);
}
See the Result Objects section for more information about the Result object.
State Object
The state object provides information about the operation status:
const { called, data, error, loading } = state;
Where:
called:trueonce the hook has run at least once.data: last successful result of typeT, otherwiseundefined.error: error of typeEif the operation failed, otherwiseundefined.loading:truewhile the operation is in progress, otherwisefalse.
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
AaveKit React provides transaction hooks 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
To send transactions, follow the steps below.
1. useSendTransaction
First, use the provider‑specific use hook to send transactions with the connected wallet.
const [sendTransaction, { loading, error }] = useSendTransaction(/* args */);
It abstracts wallet differences and provides a consistent interface that interoperates with the other AaveKit React hooks.
Import the use hook from the @aave/ entry point and wire it up with 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 an ethers Signer.
import { useSendTransaction } from "@aave/react/ethers";
import { ethers, type Signer } from "ethers";
import { useEffect, useState } from "react";
const provider = new ethers.BrowserProvider(window.ethereum);
function MyComponent({ signer }: { signer: Signer }) {
const [sendTransaction, sending] = useSendTransaction(signer);
// Use with transaction hooks…
}
function App() {
const [signer, setSigner] = useState<Signer>();
useEffect(() => {
provider.getSigner().then(setSigner);
}, []);
return signer ? <MyComponent signer={signer} /> : null;
}
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 wire it up with the Wallet of Dynamic’s primary wallet.
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(() => {
const setupWallet = async () => {
if (!httpClient || !wallets.length) return;
const accounts = await fetchWalletAccounts({ wallet: wallets[0] });
const turnkeyAccount = await createAccount({
client: httpClient,
organizationId: accounts[0].organizationId,
signWith: accounts[0].address,
});
setWalletClient(
createWalletClient({
account: turnkeyAccount,
chain: mainnet,
transport: http(),
}),
);
};
setupWallet();
}, [httpClient, wallets, fetchWalletAccounts]);
// Use with transaction hooks…
}
2. Transaction Hook
Then, instantiate the specific transaction hook for the operation you want to perform.
Call send (from use) inside the simple‑transaction hook callback, as shown below.
const [sendTransaction, sending] = useSendTransaction(/* … */);
const [execute, { loading, error }] = useSimpleTransaction((transaction) =>
sendTransaction(transaction),
);
You can bail out of the operation at any point by calling the cancel(message) function.
const [execute, { loading, error }] = useSimpleTransaction(
(transaction, { cancel }) => {
if (window.confirm("Are you sure you want to continue?") === false) {
return cancel("User cancelled the operation");
}
return sendTransaction(transaction);
},
);
Call send (from use) inside the complex transaction hook callback, as shown below.
const [sendTransaction, sending] = useSendTransaction(/* … */);
const [signTypedData] = useSignTypedData(/* … */);
const [execute, { loading, error }] = useComplexTransaction((plan) => {
switch (plan.__typename) {
case "TransactionRequest":
return sendTransaction(plan);
case "Erc20Approval":
// If token supports EIP-2612 permits, sign permit (recommended)
if (plan.bySignature) {
return signTypedData(plan.bySignature);
}
// Otherwise use traditional approval transaction
return sendTransaction(plan.byTransaction);
case "PreContractActionRequired":
return sendTransaction(plan.transaction);
}
});
You can bail out of the operation at any point by calling the cancel(message) function.
const [execute, { loading, error }] = useComplexTransaction(
(plan, { cancel }) => {
if (window.confirm("Are you sure you want to continue?") === false) {
return cancel("User cancelled the operation");
}
switch (plan.__typename) {
case "TransactionRequest":
return sendTransaction(plan);
case "Erc20Approval":
return sendTransaction(plan.byTransaction);
case "PreContractActionRequired":
return sendTransaction(plan.transaction);
}
},
);
3. Execute the Hook
Finally, execute the hook.
In your callback, call the execute function with the operation arguments and handle the result accordingly.
const result = await execute(/* args */);
if (result.isErr()) {
switch (result.error.name) {
case "CancelError":
// The user cancelled the operation
return;
case "SigningError":
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;
}
return;
}
console.log(result.value.txHash); // TxHash
In your callback, call the execute function with the operation arguments and handle the result accordingly.
const result = await execute(/* args */);
if (result.isErr()) {
switch (result.error.name) {
case "CancelError":
// The user cancelled the operation
return;
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 "ValidationError":
console.error(
"Insufficient balance:",
`required: ${result.error.cause.required.value.toDisplayString(2)}`,
`available: ${result.error.cause.available.value.toDisplayString(2)}`,
);
break;
case "UnexpectedError":
console.error(result.error.message);
break;
}
return;
}
console.log(result.value.txHash); // TxHash
You can also take a declarative approach by using the loading and error state objects to drive your UI.
function MyComponent() {
// …
return (
<div>
<button disabled={loading}>
{loading ? "Sending…" : "Send Transaction"}
</button>
{error && <p style={{ color: "red" }}>{error.message}</p>}
</div>
);
}
You now know the standard approach for handling Aave transactions with AaveKit React.
Sign Typed Data
Some Aave operations require EIP-712 typed data signatures for the following:
- ERC-20 Permits: Authorize exact amount transfers without separate approval transactions
- Swap Intents: Sign swap orders for intent-based swaps
Permits are available for ERC-20 tokens that implement EIP-2612.
As with use, instantiate the provider-specific use hook to sign typed data.
Import the use hook from the @aave/ entry point and wire it up with viem’s Wallet.
import { useSignTypedData } from "@aave/react/viem";
import { useWalletClient } from "wagmi";
function MyComponent() {
const { data: walletClient } = useWalletClient();
const [signTypedData, signing] = useSignTypedData(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 an ethers Signer.
import { useSignTypedData } from "@aave/react/ethers";
import { ethers, type Signer } from "ethers";
import { useEffect, useState } from "react";
const provider = new ethers.BrowserProvider(window.ethereum);
function MyComponent() {
const [signer, setSigner] = useState<Signer>();
const [signTypedData, signing] = useSignTypedData(signer);
useEffect(() => {
provider.getSigner().then(setSigner);
}, []);
// Use with transaction hooks…
}
Import the use hook from the @aave/ entry point.
import { useSignTypedData } from "@aave/react/privy";
function MyComponent() {
const [signTypedData, signing] = useSignTypedData();
// Use with transaction hooks…
}
Import the use hook from the @aave/ entry point and wire it up with the Wallet of Dynamic’s primary wallet.
import { useSignTypedData } 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 [signTypedData, signing] = useSignTypedData(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 { useSignTypedData } from "@aave/react/thirdweb";
function MyComponent() {
const [signTypedData, signing] = useSignTypedData();
// 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 { useSignTypedData } 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 [signTypedData, signing] = useSignTypedData(walletClient);
useEffect(() => {
const setupWallet = async () => {
if (!httpClient || !wallets.length) return;
const accounts = await fetchWalletAccounts({ wallet: wallets[0] });
const turnkeyAccount = await createAccount({
client: httpClient,
organizationId: accounts[0].organizationId,
signWith: accounts[0].address,
});
setWalletClient(
createWalletClient({
account: turnkeyAccount,
chain: mainnet,
transport: http(),
}),
);
};
setupWallet();
}, [httpClient, wallets, fetchWalletAccounts]);
// Use with transaction hooks…
}
Then, use the sign function as described in the specific operation guide.
Result Objects
AaveKit handles errors with Result<T, E>, which represents one of two states:
Ok<T>: A successful result containing a value of typeTErr<E>: A failure containing an error of typeE
import { ok, err, Result } from "@aave/react";
function parseNumber(input: string): Result<number, Error> {
return isNaN(Number(input))
? err(new Error("Invalid number"))
: ok(Number(input));
}
With a Result<T, E>, use the is and is methods to check the outcome and narrow the type.
const result = parseNumber("123");
if (result.isOk()) {
console.log(result.value); // 123
} else {
console.error(result.error); // Error
}
This approach avoids reliance on try/ blocks and promotes predictable, type-safe code by ensuring errors are handled explicitly.
AaveKit builds its Result object on
NeverThrow.
Result is the async, thenable variant of Result<T, E>. Awaiting it resolves to a Result<T, E>.
import { ResultAsync } from "@aave/react";
function fetchUser(id: number): ResultAsync<{ name: string }, Error> {
return ResultAsync.fromPromise(
fetch(`/api/users/${id}`).then((r) => r.json()),
() => new Error("Not found"),
);
}
const result = await fetchUser(1);
if (result.isOk()) {
console.log(result.value); // { name: "John" }
} else {
console.error(result.error); // Error
}