Skip to content

React Hooks

@earnforge/react provides 10 hooks built on top of @earnforge/sdk, wagmi, and TanStack Query. Each hook handles loading states, errors, caching, and re-fetching automatically.

Terminal window
npm i @earnforge/react @earnforge/sdk @tanstack/react-query
Package Version
@earnforge/sdk ^0.1.0
@tanstack/react-query ^5.90.0
react ^18.0.0 || ^19.0.0

Wrap your application with EarnForgeProvider to make the SDK instance available to all hooks.

The Earn API requires a key, and hooks run in the browser, so the browser must never hold it. Proxy through a server route instead; see Studio’s implementation for a route handler with a path allowlist.

import { createEarnForge } from '@earnforge/sdk';
import { EarnForgeProvider } from '@earnforge/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// NEVER `NEXT_PUBLIC_`. That ships the key to every visitor's browser, where
// it is readable from the network tab. Point the SDK at your own route handler
// and let the server attach the real key.
const forge = createEarnForge({
earnData: { baseUrl: '/api/earn' },
});
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<EarnForgeProvider sdk={forge}>
<YourApp />
</EarnForgeProvider>
</QueryClientProvider>
);
}

Fetch a paginated list of vaults with optional filters.

import { useVaults } from '@earnforge/react';
function VaultList() {
const { data, isLoading, error, fetchMore, hasMore } = useVaults({
chainId: 8453,
asset: 'USDC',
minTvl: 1_000_000,
limit: 20,
strategy: 'conservative',
});
if (isLoading) return <p>Loading vaults...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data?.map((vault) => (
<div key={vault.slug}>
<h3>{vault.name}</h3>
<p>APY: {vault.analytics.apy.total}%</p>
</div>
))}
{hasMore && <button onClick={fetchMore}>Load more</button>}
</div>
);
}

Parameters:

Field Type Description
chainId number Filter by chain ID
asset string Filter by asset symbol
minTvl number Minimum TVL in USD
sortBy string Sort field
limit number Max vaults to load
strategy StrategyPreset Strategy preset filter

Returns:

Field Type Description
data Vault[] | undefined Loaded vaults
isLoading boolean Initial loading state
error Error | null Error if failed
fetchMore () => void Load next page
hasMore boolean Whether more pages exist

Fetch a single vault by slug.

import { useVault } from '@earnforge/react';
function VaultDetail({ slug }: { slug: string }) {
const { data: vault, isLoading, error } = useVault(slug);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!vault) return null;
return (
<div>
<h2>{vault.name}</h2>
<p>APY: {vault.analytics.apy.total}%</p>
<p>Protocol: {vault.protocol.name}</p>
</div>
);
}

Pass undefined as the slug to disable the query (useful for conditional fetching).


Fetch top-yielding vaults sorted by APY.

import { useEarnTopYield } from '@earnforge/react';
function TopYields() {
const { data, isLoading } = useEarnTopYield({
asset: 'USDC',
limit: 5,
strategy: 'risk-adjusted',
});
return (
<ul>
{data?.map((vault) => (
<li key={vault.slug}>
{vault.name} -- {vault.analytics.apy.total}%
</li>
))}
</ul>
);
}

Parameters:

Field Type Description
asset string Filter by asset symbol
chainId number Filter by chain ID
limit number Max vaults
strategy StrategyPreset Strategy preset
minTvl number Minimum TVL in USD

Fetch portfolio positions for a wallet address.

import { usePortfolio } from '@earnforge/react';
function Portfolio({ wallet }: { wallet: string }) {
const { data, isLoading, error } = usePortfolio(wallet);
if (isLoading) return <p>Loading portfolio...</p>;
if (!data) return null;
return (
<div>
{data.positions.map((pos, i) => (
<div key={i}>
{pos.vault.name}: ${pos.balanceUsd}
</div>
))}
</div>
);
}

Pass undefined as the wallet to disable the query.


Compute a risk score for a vault. This is a synchronous computation wrapped in useMemo for referential stability.

import { useRiskScore } from '@earnforge/react';
function RiskBadge({ vault }: { vault: Vault }) {
const { data: risk } = useRiskScore(vault);
if (!risk) return null;
const color = risk.label === 'low' ? 'green'
: risk.label === 'medium' ? 'yellow'
: 'red';
return (
<span style={{ color }}>
{risk.score}/10 ({risk.label})
</span>
);
}

Resolve a strategy preset into its filter configuration.

import { useStrategy, useVaults } from '@earnforge/react';
function StrategyVaults({ preset }: { preset: StrategyPreset }) {
const { filters } = useStrategy(preset);
const { data } = useVaults({ ...filters });
return (
<div>
<h2>{preset} strategy</h2>
{data?.map((v) => <p key={v.slug}>{v.name}</p>)}
</div>
);
}

Returns:

Field Type Description
data StrategyConfig | undefined Full strategy config
filters object | undefined Filters to pass to useVaults
sort string | undefined Sort field
sortDirection 'asc' | 'desc' | undefined Sort direction

Get a suggested portfolio allocation.

import { useSuggest } from '@earnforge/react';
function Suggestions() {
const { data, isLoading } = useSuggest({
amount: 10_000,
asset: 'USDC',
maxChains: 3,
strategy: 'diversified',
});
if (isLoading) return <p>Computing allocations...</p>;
if (!data) return null;
return (
<div>
<p>Expected APY: {data.expectedApy}%</p>
{data.allocations.map((alloc, i) => (
<div key={i}>
{alloc.vault.name}: ${alloc.amount} ({alloc.percentage}%)
</div>
))}
</div>
);
}

Pass undefined or set amount to 0 to disable the query.


Fetch 30-day APY history from DeFiLlama.

import { useApyHistory } from '@earnforge/react';
function ApyChart({ address, chainId }: { address: string; chainId: number }) {
const { data, isLoading } = useApyHistory(address, chainId);
if (isLoading) return <p>Loading history...</p>;
return (
<ul>
{data?.map((point, i) => (
<li key={i}>{point.date}: {point.apy}%</li>
))}
</ul>
);
}

Pass undefined for either parameter to disable the query.


Full deposit state machine hook. Handles preflight checks, quote building, and transaction sending through distinct phases.

import { useEarnDeposit } from '@earnforge/react';
import { useSendTransaction, useAccount } from 'wagmi';
function DepositForm({ vault }: { vault: Vault }) {
const { address } = useAccount();
const { sendTransactionAsync } = useSendTransaction();
const { state, prepare, execute, reset } = useEarnDeposit({
vault,
amount: '100',
wallet: address ?? '',
sendTransactionAsync,
});
return (
<div>
<p>Phase: {state.phase}</p>
{state.phase === 'idle' && (
<button onClick={prepare}>Prepare Deposit</button>
)}
{state.phase === 'ready' && state.quote && (
<div>
<p>Quote ready: {state.quote.humanAmount} tokens</p>
<button onClick={execute}>Confirm Deposit</button>
</div>
)}
{state.phase === 'success' && (
<p>Transaction sent: {state.txHash}</p>
)}
{state.phase === 'error' && (
<div>
<p>Error: {state.error?.message}</p>
<button onClick={reset}>Try Again</button>
</div>
)}
</div>
);
}

The deposit flow progresses through these phases:

idle --> preflight --> quoting --> ready --> sending --> success
\ | | | |
\________|____________|__________|__________|--> error
Phase Description
idle Initial state. Call prepare() to start.
preflight Running pitfall checks (isTransactional, chain, gas, etc.)
quoting Building the deposit quote via Composer API
ready Quote ready. Call execute() to send the transaction.
sending Transaction submitted, waiting for hash
success Transaction hash received
error Something failed. state.error contains the Error. Call reset() to return to idle.
Field Type Description
state.phase DepositPhase Current phase
state.preflightReport PreflightReport | null Preflight result
state.quote DepositQuoteResult | null Built quote
state.txHash string | null Transaction hash on success
state.error Error | null Error details
prepare () => Promise<void> Start preflight + quote flow
execute () => Promise<void> Send the transaction
reset () => void Reset to idle

The exit path, and the mirror of useEarnDeposit: same phase machine, same prepare / execute / reset shape. Redeems vault shares back to an underlying token, optionally on a different chain.

import { useEarnRedeem } from '@earnforge/react';
import { useSendTransaction, useAccount } from 'wagmi';
function RedeemForm({ vault }: { vault: Vault }) {
const { address } = useAccount();
const { sendTransactionAsync } = useSendTransaction();
const { state, prepare, execute, reset } = useEarnRedeem({
vault,
amount: '100', // vault SHARES, not underlying
wallet: address ?? '',
sendTransactionAsync,
});
if (state.phase === 'ready') {
return <button onClick={execute}>Confirm Redeem</button>;
}
return <button onClick={prepare}>Prepare Redeem</button>;
}
Field Type Description
vault Vault | undefined Vault to redeem from
amount string Vault share tokens to redeem, human-readable
wallet string Wallet address
toToken string? Token to receive. Defaults to the underlying token on the vault chain.
toChain number? Destination chain. Defaults to the vault chain.
slippage number? Slippage tolerance (0.03 = 3%)
sendTransactionAsync function? From wagmi’s useSendTransaction()
Field Type Description
state RedeemState Same phase machine as useEarnDeposit
prepare () => Promise<void> Start preflight + quote flow
execute () => Promise<void> Send the transaction
reset () => void Reset to idle

Returns the underlying EarnForge SDK instance from context. Every other hook is built on it; reach for this when you need something the hooks do not cover, such as watch() or optimizeGasRoutes().

import { useEarnForge } from '@earnforge/react';
function GasRoutes({ vault }: { vault: Vault }) {
const forge = useEarnForge();
useEffect(() => {
forge.optimizeGasRoutes(vault, { amount: '1000', wallet: '0x...' })
.then(setRoutes);
}, [forge, vault]);
}

Throws if called outside an <EarnForgeProvider>, so a missing provider fails at the call site rather than surfacing later as an undefined client.