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.
Installation
Section titled “Installation”npm i @earnforge/react @earnforge/sdk @tanstack/react-querypnpm add @earnforge/react @earnforge/sdk @tanstack/react-queryPeer Dependencies
Section titled “Peer Dependencies”| Package | Version |
|---|---|
@earnforge/sdk |
^0.1.0 |
@tanstack/react-query |
^5.90.0 |
react |
^18.0.0 || ^19.0.0 |
EarnForgeProvider
Section titled “EarnForgeProvider”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> );}Hook Reference
Section titled “Hook Reference”useVaults
Section titled “useVaults”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 |
useVault
Section titled “useVault”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).
useEarnTopYield
Section titled “useEarnTopYield”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 |
usePortfolio
Section titled “usePortfolio”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.
useRiskScore
Section titled “useRiskScore”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> );}useStrategy
Section titled “useStrategy”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 |
useSuggest
Section titled “useSuggest”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.
useApyHistory
Section titled “useApyHistory”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.
useEarnDeposit
Section titled “useEarnDeposit”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> );}State Machine
Section titled “State Machine”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. |
Return Value
Section titled “Return Value”| 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 |
useEarnRedeem
Section titled “useEarnRedeem”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>;}Parameters
Section titled “Parameters”| 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() |
Return Value
Section titled “Return Value”| 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 |
useEarnForge
Section titled “useEarnForge”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.