Skip to content

React Hooks

@earnforge/react provides 9 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
PackageVersion
@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.

import { createEarnForge } from '@earnforge/sdk';
import { EarnForgeProvider } from '@earnforge/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const forge = createEarnForge({
composerApiKey: process.env.NEXT_PUBLIC_LIFI_API_KEY,
});
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:

FieldTypeDescription
chainIdnumberFilter by chain ID
assetstringFilter by asset symbol
minTvlnumberMinimum TVL in USD
sortBystringSort field
limitnumberMax vaults to load
strategyStrategyPresetStrategy preset filter

Returns:

FieldTypeDescription
dataVault[] | undefinedLoaded vaults
isLoadingbooleanInitial loading state
errorError | nullError if failed
fetchMore() => voidLoad next page
hasMorebooleanWhether 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:

FieldTypeDescription
assetstringFilter by asset symbol
chainIdnumberFilter by chain ID
limitnumberMax vaults
strategyStrategyPresetStrategy preset
minTvlnumberMinimum 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:

FieldTypeDescription
dataStrategyConfig | undefinedFull strategy config
filtersobject | undefinedFilters to pass to useVaults
sortstring | undefinedSort field
sortDirection'asc' | 'desc' | undefinedSort 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
PhaseDescription
idleInitial state. Call prepare() to start.
preflightRunning pitfall checks (isTransactional, chain, gas, etc.)
quotingBuilding the deposit quote via Composer API
readyQuote ready. Call execute() to send the transaction.
sendingTransaction submitted, waiting for hash
successTransaction hash received
errorSomething failed. state.error contains the Error. Call reset() to return to idle.
FieldTypeDescription
state.phaseDepositPhaseCurrent phase
state.preflightReportPreflightReport | nullPreflight result
state.quoteDepositQuoteResult | nullBuilt quote
state.txHashstring | nullTransaction hash on success
state.errorError | nullError details
prepare() => Promise<void>Start preflight + quote flow
execute() => Promise<void>Send the transaction
reset() => voidReset to idle