Overview
The usePortfolio hook fetches comprehensive portfolio data for a Stacks wallet address. It automatically refreshes every 30 seconds to keep balances and transaction history up-to-date.
Source: src/hooks/usePortfolio.js
Import
import { usePortfolio } from './hooks/usePortfolio' ;
Hook Signature
function usePortfolio ( address : string | null ) : {
portfolio : {
stxBalance : string ,
sbtcBalance : string ,
totalUSD : string ,
txHistory : Array ,
stxPrice : number ,
},
loading : boolean ,
error : string | null ,
}
Parameters
The Stacks wallet address to fetch portfolio data for. If null or undefined, the hook will not fetch data. Example: "SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5"
Return Values
Complete portfolio data object containing all wallet information: STX token balance formatted as a string (e.g., "100.5000"). Shows "--" while loading or if fetch fails.
sBTC token balance formatted as a string with full precision (e.g., "0.00500000"). Shows "--" while loading.
Total portfolio value in USD (e.g., "285.50"). Calculated from STX and sBTC balances at current market prices. Shows "--" while loading.
Array of recent transaction objects. Each transaction includes:
txid: Transaction hash
tx_status: "success", "pending", or "failed"
tx_type: Type of transaction (e.g., "token_transfer", "contract_call")
sender_address: Address that initiated the transaction
block_height: Block number (if confirmed)
burn_block_time_iso: Timestamp in ISO format
Empty array [] while loading or if no transactions found. Current STX price in USD (e.g., 2.85). Used to calculate totalUSD. Defaults to 0 while loading.
true during initial fetch or refresh, false once data is loaded. Use this to show loading skeletons.
Error message if portfolio fetch fails (e.g., "Failed to load portfolio data"). null if no error.
Usage Example
Basic Usage
With Loading Skeleton
With Transaction History
Full Dashboard Integration
import { useWallet } from './hooks/useWallet' ;
import { usePortfolio } from './hooks/usePortfolio' ;
function PortfolioDisplay () {
const { address } = useWallet ();
const { portfolio , loading , error } = usePortfolio ( address );
if ( loading ) {
return < div > Loading portfolio... </ div > ;
}
if ( error ) {
return < div className = "error" > { error } </ div > ;
}
return (
< div className = "portfolio" >
< h2 > Your Portfolio </ h2 >
< p > STX Balance: { portfolio . stxBalance } STX </ p >
< p > sBTC Balance: { portfolio . sbtcBalance } sBTC </ p >
< p > Total Value: $ { portfolio . totalUSD } </ p >
< p > STX Price: $ { portfolio . stxPrice } </ p >
</ div >
);
}
import { useWallet } from './hooks/useWallet' ;
import { usePortfolio } from './hooks/usePortfolio' ;
function PortfolioCard () {
const { connected , address } = useWallet ();
const { portfolio , loading , error } = usePortfolio ( address );
if ( ! connected ) {
return (
< div className = "card empty" >
< p > Connect your wallet to view portfolio </ p >
</ div >
);
}
return (
< div className = "portfolio-card" >
< h2 > Portfolio Summary </ h2 >
{ loading ? (
< div className = "skeleton" >
< div className = "skeleton-line" />
< div className = "skeleton-line" />
< div className = "skeleton-line" />
</ div >
) : error ? (
< div className = "error-state" >
< span > ⚠️ </ span >
< p > { error } </ p >
</ div >
) : (
< div className = "portfolio-grid" >
< div className = "balance-card" >
< label > STX Balance </ label >
< p className = "amount" > { portfolio . stxBalance } </ p >
</ div >
< div className = "balance-card" >
< label > sBTC Balance </ label >
< p className = "amount" > { portfolio . sbtcBalance } </ p >
</ div >
< div className = "balance-card highlight" >
< label > Total Value </ label >
< p className = "amount" > $ { portfolio . totalUSD } </ p >
</ div >
</ div >
) }
</ div >
);
}
import { usePortfolio } from './hooks/usePortfolio' ;
function PortfolioDashboard ({ address }) {
const { portfolio , loading , error } = usePortfolio ( address );
if ( loading ) return < div > Loading... </ div > ;
if ( error ) return < div > Error: { error } </ div > ;
return (
< div className = "dashboard" >
{ /* Balances Section */ }
< section className = "balances" >
< h2 > Balances </ h2 >
< div className = "grid" >
< div className = "balance-item" >
< span > STX </ span >
< strong > { portfolio . stxBalance } </ strong >
</ div >
< div className = "balance-item" >
< span > sBTC </ span >
< strong > { portfolio . sbtcBalance } </ strong >
</ div >
< div className = "balance-item" >
< span > Total USD </ span >
< strong > $ { portfolio . totalUSD } </ strong >
</ div >
</ div >
</ section >
{ /* Transaction History */ }
< section className = "transactions" >
< h2 > Recent Transactions </ h2 >
{ portfolio . txHistory . length === 0 ? (
< p > No transactions yet </ p >
) : (
< ul className = "tx-list" >
{ portfolio . txHistory . map ( tx => (
< li key = { tx . txid } className = "tx-item" >
< div className = "tx-info" >
< span className = "tx-type" > { tx . tx_type } </ span >
< span className = { `tx-status ${ tx . tx_status } ` } >
{ tx . tx_status }
</ span >
</ div >
< div className = "tx-details" >
< code > { tx . txid . slice ( 0 , 10 ) } ... { tx . txid . slice ( - 6 ) } </ code >
< time > {new Date ( tx . burn_block_time_iso ). toLocaleString () } </ time >
</ div >
</ li >
)) }
</ ul >
) }
</ section >
</ div >
);
}
import { useWallet } from './hooks/useWallet' ;
import { usePortfolio } from './hooks/usePortfolio' ;
import { useAIAdvisor } from './hooks/useAIAdvisor' ;
function Dashboard () {
const { connected , address , connectWallet } = useWallet ();
const { portfolio , loading , error } = usePortfolio ( address );
const { strategy , fetchStrategy } = useAIAdvisor ({
address ,
stxBalance: portfolio . stxBalance ,
sbtcBalance: portfolio . sbtcBalance ,
totalUSD: portfolio . totalUSD ,
});
if ( ! connected ) {
return (
< div className = "connect-screen" >
< h1 > Welcome to Staxiq </ h1 >
< button onClick = { connectWallet } > Connect Wallet </ button >
</ div >
);
}
return (
< div className = "dashboard-layout" >
{ /* Portfolio Overview */ }
< header className = "portfolio-header" >
< h1 > Portfolio </ h1 >
{ loading ? (
< div className = "loading" > Updating... </ div >
) : (
< div className = "portfolio-value" >
< span className = "label" > Total Value </ span >
< span className = "amount" > $ { portfolio . totalUSD } </ span >
</ div >
) }
</ header >
{ /* Balance Cards */ }
< section className = "balances" >
< div className = "card" >
< h3 > STX </ h3 >
< p className = "balance" > { portfolio . stxBalance } </ p >
< p className = "price" > $ { portfolio . stxPrice } per STX </ p >
</ div >
< div className = "card" >
< h3 > sBTC </ h3 >
< p className = "balance" > { portfolio . sbtcBalance } </ p >
</ div >
</ section >
{ /* AI Strategy */ }
< section className = "ai-section" >
< button onClick = { fetchStrategy } > Get AI Strategy </ button >
{ strategy && < div className = "strategy" > { strategy } </ div > }
</ section >
{ /* Recent Activity */ }
< section className = "activity" >
< h2 > Recent Activity </ h2 >
< ul >
{ portfolio . txHistory . slice ( 0 , 5 ). map ( tx => (
< li key = { tx . txid } >
{ tx . tx_type } · { tx . tx_status }
</ li >
)) }
</ ul >
</ section >
</ div >
);
}
Auto-Refresh Behavior
The hook automatically refreshes portfolio data every 30 seconds:
useEffect (() => {
if ( ! address ) return ;
async function fetchPortfolio () {
// Fetch logic...
}
fetchPortfolio (); // Initial fetch
const interval = setInterval ( fetchPortfolio , 30000 ); // Refresh every 30s
return () => clearInterval ( interval ); // Cleanup on unmount
}, [ address ]);
The auto-refresh ensures users always see up-to-date balances without manual page refreshes. This is especially useful for monitoring pending transactions or deposit confirmations.
Loading States
The hook provides clear loading states throughout the data lifecycle:
Initial State
Before address is provided:
loading is false
All portfolio values show "--"
txHistory is empty []
Fetching State
While fetching data:
loading is true
Previous portfolio values remain visible (or "--" on first load)
UI should show loading indicators
Success State
After successful fetch:
loading is false
All portfolio values are populated
error is null
Error State
If fetch fails:
loading is false
error contains error message
Portfolio values show "--"
Error Handling
The hook catches and formats errors from the Stacks API:
try {
setLoading ( true );
setError ( null );
const data = await getFullPortfolio ( address );
setPortfolio ( data );
} catch ( err ) {
setError ( 'Failed to load portfolio data' );
console . error ( err );
} finally {
setLoading ( false );
}
Common errors:
Network request timeout
Invalid address format
Stacks API rate limiting
Blockchain node unavailable
Portfolio data comes from the getFullPortfolio service:
import { getFullPortfolio } from '../services/stacksApi' ;
// Returns:
{
stxBalance : "100.5000" ,
sbtcBalance : "0.00500000" ,
totalUSD : "285.50" ,
stxPrice : 2.85 ,
txHistory : [
{
txid: "0x1234..." ,
tx_status: "success" ,
tx_type: "token_transfer" ,
sender_address: "SP2H8..." ,
block_height: 123456 ,
burn_block_time_iso: "2024-03-09T12:00:00.000Z" ,
},
// ... more transactions
]
}
Best Practices
Check Address Always ensure address is valid before rendering portfolio data. Show a connect prompt if null.
Loading Skeletons Use loading state to show skeleton loaders instead of empty states during fetch.
Error Display Always display the error message to users when portfolio fetch fails. Include a retry option.
Auto-Refresh Awareness Remember data refreshes every 30 seconds. Consider showing a “last updated” timestamp.
Common Patterns
Conditional Rendering Based on Connection
function Portfolio () {
const { connected , address } = useWallet ();
const { portfolio , loading } = usePortfolio ( connected ? address : null );
if ( ! connected ) {
return < ConnectPrompt /> ;
}
if ( loading ) {
return < LoadingSkeleton /> ;
}
return < PortfolioDisplay data = { portfolio } /> ;
}
Passing Portfolio to Other Hooks
function Dashboard () {
const { address } = useWallet ();
const { portfolio } = usePortfolio ( address );
// Pass portfolio data to AI advisor
const { strategy } = useAIAdvisor ({
address ,
stxBalance: portfolio . stxBalance ,
sbtcBalance: portfolio . sbtcBalance ,
totalUSD: portfolio . totalUSD ,
});
return < div > { /* ... */ } </ div > ;
}
Manual Refresh Trigger
import { useState , useEffect } from 'react' ;
import { getFullPortfolio } from './services/stacksApi' ;
function PortfolioWithRefresh ({ address }) {
const { portfolio , loading } = usePortfolio ( address );
const [ refreshing , setRefreshing ] = useState ( false );
async function handleManualRefresh () {
setRefreshing ( true );
// Force immediate refresh by calling API directly
await getFullPortfolio ( address );
setRefreshing ( false );
}
return (
< div >
< button onClick = { handleManualRefresh } disabled = { refreshing } >
{ refreshing ? 'Refreshing...' : 'Refresh Now' }
</ button >
{ /* Portfolio display */ }
</ div >
);
}
The hook refreshes every 30 seconds. If you have multiple components using usePortfolio with the same address, consider lifting the hook to a parent component and passing data as props to avoid duplicate API calls.
// Good: Single hook instance
function App () {
const { address } = useWallet ();
const { portfolio , loading } = usePortfolio ( address );
return (
< div >
< PortfolioHeader data = { portfolio } loading = { loading } />
< PortfolioCards data = { portfolio } loading = { loading } />
< AIAdvisor portfolio = { portfolio } />
</ div >
);
}
// Bad: Multiple hook instances (3x API calls)
function App () {
return (
< div >
< PortfolioHeader /> { /* calls usePortfolio */ }
< PortfolioCards /> { /* calls usePortfolio */ }
< AIAdvisor /> { /* calls usePortfolio */ }
</ div >
);
}
Stacks API - Underlying portfolio data service
AI Service - Uses portfolio data for strategy generation