Picture this: You're tracking your yield farming positions across five different protocols, switching between tabs faster than a day trader on espresso. Sound familiar? Building a unified yield farming dashboard solves this chaos by bringing all your DeFi data into one sleek interface.
This guide shows you how to create a professional yield farming dashboard using React.js and The Graph Protocol. You'll learn to fetch real-time blockchain data, visualize yield metrics, and build interactive components that make tracking DeFi positions effortless.
Why Build a Custom Yield Farming Dashboard?
Most DeFi users juggle multiple platforms to track their investments. A custom dashboard eliminates this friction by providing:
- Real-time yield tracking across multiple protocols
- Portfolio performance metrics in one view
- Custom alerts for yield rate changes
- Historical Data Analysis for better decisions
The Graph Protocol makes this possible by indexing blockchain data into queryable APIs. Combined with React.js, you get a powerful foundation for DeFi applications.
Setting Up Your React.js Development Environment
First, create a new React application with the necessary dependencies:
npx create-react-app yield-farming-dashboard
cd yield-farming-dashboard
npm install @apollo/client graphql recharts web3 ethers
Install additional UI libraries for better user experience:
npm install @mui/material @emotion/react @emotion/styled
npm install @mui/icons-material lucide-react
Your project structure should look like this:
yield-farming-dashboard/
├── src/
│ ├── components/
│ │ ├── Dashboard.js
│ │ ├── PoolCard.js
│ │ └── YieldChart.js
│ ├── hooks/
│ │ └── useYieldData.js
│ ├── graphql/
│ │ └── queries.js
│ └── utils/
│ └── formatters.js
Understanding The Graph Protocol for DeFi Data
The Graph Protocol indexes blockchain data into subgraphs that you can query with GraphQL. For yield farming, you'll primarily use subgraphs from popular protocols like Uniswap, Compound, and Aave.
Key Concepts:
- Subgraph: A custom API that indexes specific smart contract events
- Entity: Data objects like pools, users, or transactions
- Query: GraphQL requests to fetch specific data
Popular DeFi subgraphs include:
- Uniswap V3:
https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3 - Compound:
https://api.thegraph.com/subgraphs/name/graphprotocol/compound-v2 - Aave:
https://api.thegraph.com/subgraphs/name/aave/protocol-v2
Creating GraphQL Queries for Yield Data
Create a queries file to organize your GraphQL operations:
// src/graphql/queries.js
import { gql } from '@apollo/client';
export const GET_UNISWAP_POOLS = gql`
query GetUniswapPools($first: Int!, $orderBy: String!, $orderDirection: String!) {
pools(
first: $first
orderBy: $orderBy
orderDirection: $orderDirection
where: { volumeUSD_gt: "10000" }
) {
id
token0 {
id
symbol
name
}
token1 {
id
symbol
name
}
feeTier
liquidity
volumeUSD
totalValueLockedUSD
apr: feeGrowthGlobal0X128
}
}
`;
export const GET_USER_POSITIONS = gql`
query GetUserPositions($user: String!) {
positions(where: { owner: $user }) {
id
owner
pool {
id
token0 {
symbol
}
token1 {
symbol
}
}
liquidity
depositedToken0
depositedToken1
collectedFeesToken0
collectedFeesToken1
}
}
`;
export const GET_HISTORICAL_DATA = gql`
query GetHistoricalData($poolId: String!, $first: Int!) {
poolDayDatas(
first: $first
orderBy: date
orderDirection: desc
where: { pool: $poolId }
) {
date
volumeUSD
tvlUSD
feesUSD
}
}
`;
Setting Up Apollo Client for Graph Integration
Configure Apollo Client to connect with The Graph endpoints:
// src/apollo/client.js
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
const httpLink = createHttpLink({
uri: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
});
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
pollInterval: 30000, // Poll every 30 seconds
},
},
});
export default client;
Wrap your app with the Apollo Provider:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import client from './apollo/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
Building the Dashboard Components
Main Dashboard Component
Create the main dashboard that orchestrates all components:
// src/components/Dashboard.js
import React, { useState, useEffect } from 'react';
import { Grid, Container, Typography, Box } from '@mui/material';
import { useQuery } from '@apollo/client';
import { GET_UNISWAP_POOLS } from '../graphql/queries';
import PoolCard from './PoolCard';
import YieldChart from './YieldChart';
import { formatCurrency } from '../utils/formatters';
const Dashboard = () => {
const [selectedPool, setSelectedPool] = useState(null);
const { data, loading, error } = useQuery(GET_UNISWAP_POOLS, {
variables: {
first: 20,
orderBy: 'totalValueLockedUSD',
orderDirection: 'desc'
}
});
if (loading) return <Typography>Loading pools...</Typography>;
if (error) return <Typography>Error: {error.message}</Typography>;
return (
<Container maxWidth="xl">
<Box sx={{ my: 4 }}>
<Typography variant="h3" component="h1" gutterBottom>
Yield Farming Dashboard
</Typography>
<Grid container spacing={3}>
{/* Pool Cards */}
<Grid item xs={12} md={8}>
<Typography variant="h5" sx={{ mb: 2 }}>
Top Yield Farming Pools
</Typography>
<Grid container spacing={2}>
{data.pools.map((pool) => (
<Grid item xs={12} sm={6} md={4} key={pool.id}>
<PoolCard
pool={pool}
onClick={() => setSelectedPool(pool)}
isSelected={selectedPool?.id === pool.id}
/>
</Grid>
))}
</Grid>
</Grid>
{/* Chart Section */}
<Grid item xs={12} md={4}>
{selectedPool && (
<YieldChart poolId={selectedPool.id} />
)}
</Grid>
</Grid>
</Box>
</Container>
);
};
export default Dashboard;
Pool Card Component
Create reusable cards for each yield farming pool:
// src/components/PoolCard.js
import React from 'react';
import { Card, CardContent, Typography, Box, Chip } from '@mui/material';
import { TrendingUp, DollarSign } from 'lucide-react';
import { formatCurrency, calculateAPR } from '../utils/formatters';
const PoolCard = ({ pool, onClick, isSelected }) => {
const apr = calculateAPR(pool.volumeUSD, pool.totalValueLockedUSD);
return (
<Card
sx={{
cursor: 'pointer',
border: isSelected ? '2px solid #2196f3' : '1px solid #e0e0e0',
'&:hover': {
boxShadow: 3
}
}}
onClick={onClick}
>
<CardContent>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6" component="h3">
{pool.token0.symbol}/{pool.token1.symbol}
</Typography>
<Chip
label={`${pool.feeTier / 10000}%`}
size="small"
color="primary"
/>
</Box>
<Box sx={{ mt: 2 }}>
<Box display="flex" alignItems="center" gap={1}>
<TrendingUp size={16} />
<Typography variant="body2" color="text.secondary">
APR: {apr.toFixed(2)}%
</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1} sx={{ mt: 1 }}>
<DollarSign size={16} />
<Typography variant="body2" color="text.secondary">
TVL: {formatCurrency(pool.totalValueLockedUSD)}
</Typography>
</Box>
</Box>
</CardContent>
</Card>
);
};
export default PoolCard;
Yield Chart Component
Build an interactive chart to visualize yield performance:
// src/components/YieldChart.js
import React from 'react';
import { useQuery } from '@apollo/client';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { Paper, Typography, Box } from '@mui/material';
import { GET_HISTORICAL_DATA } from '../graphql/queries';
const YieldChart = ({ poolId }) => {
const { data, loading, error } = useQuery(GET_HISTORICAL_DATA, {
variables: {
poolId,
first: 30
}
});
if (loading) return <Typography>Loading chart...</Typography>;
if (error) return <Typography>Error loading chart data</Typography>;
const chartData = data.poolDayDatas.map(day => ({
date: new Date(day.date * 1000).toLocaleDateString(),
yield: (day.feesUSD / day.tvlUSD) * 365 * 100, // Annualized yield
volume: day.volumeUSD / 1000000 // Volume in millions
}));
return (
<Paper sx={{ p: 2 }}>
<Typography variant="h6" sx={{ mb: 2 }}>
Historical Yield Performance
</Typography>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip
formatter={(value, name) => [
name === 'yield' ? `${value.toFixed(2)}%` : `$${value.toFixed(2)}M`,
name === 'yield' ? 'APR' : 'Volume'
]}
/>
<Line
type="monotone"
dataKey="yield"
stroke="#2196f3"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</Paper>
);
};
export default YieldChart;
Adding Real-time Updates with React Hooks
Create a custom hook to manage yield data updates:
// src/hooks/useYieldData.js
import { useState, useEffect } from 'react';
import { useQuery } from '@apollo/client';
import { GET_UNISWAP_POOLS } from '../graphql/queries';
const useYieldData = (refreshInterval = 30000) => {
const [pools, setPools] = useState([]);
const [totalTVL, setTotalTVL] = useState(0);
const { data, loading, error, refetch } = useQuery(GET_UNISWAP_POOLS, {
variables: {
first: 100,
orderBy: 'totalValueLockedUSD',
orderDirection: 'desc'
},
pollInterval: refreshInterval
});
useEffect(() => {
if (data?.pools) {
setPools(data.pools);
const tvl = data.pools.reduce((sum, pool) =>
sum + parseFloat(pool.totalValueLockedUSD), 0
);
setTotalTVL(tvl);
}
}, [data]);
const refreshData = () => {
refetch();
};
return {
pools,
totalTVL,
loading,
error,
refreshData
};
};
export default useYieldData;
Implementing User Portfolio Tracking
Add wallet connection and portfolio tracking:
// src/components/Portfolio.js
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { useQuery } from '@apollo/client';
import { GET_USER_POSITIONS } from '../graphql/queries';
import { Button, Typography, Box, Alert } from '@mui/material';
const Portfolio = () => {
const [userAddress, setUserAddress] = useState(null);
const [connected, setConnected] = useState(false);
const { data, loading, error } = useQuery(GET_USER_POSITIONS, {
variables: { user: userAddress },
skip: !userAddress
});
const connectWallet = async () => {
try {
if (window.ethereum) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send('eth_requestAccounts', []);
const signer = provider.getSigner();
const address = await signer.getAddress();
setUserAddress(address);
setConnected(true);
} else {
alert('Please install MetaMask');
}
} catch (error) {
console.error('Error connecting wallet:', error);
}
};
if (!connected) {
return (
<Box sx={{ textAlign: 'center', mt: 4 }}>
<Typography variant="h5" sx={{ mb: 2 }}>
Connect Your Wallet
</Typography>
<Button variant="contained" onClick={connectWallet}>
Connect MetaMask
</Button>
</Box>
);
}
if (loading) return <Typography>Loading positions...</Typography>;
if (error) return <Alert severity="error">Error: {error.message}</Alert>;
return (
<Box>
<Typography variant="h5" sx={{ mb: 2 }}>
Your Positions
</Typography>
{data?.positions?.length > 0 ? (
data.positions.map((position) => (
<Box key={position.id} sx={{ mb: 2, p: 2, border: '1px solid #e0e0e0' }}>
<Typography variant="h6">
{position.pool.token0.symbol}/{position.pool.token1.symbol}
</Typography>
<Typography>
Liquidity: {position.liquidity}
</Typography>
<Typography>
Fees Collected: {position.collectedFeesToken0} / {position.collectedFeesToken1}
</Typography>
</Box>
))
) : (
<Typography>No positions found</Typography>
)}
</Box>
);
};
export default Portfolio;
Utility Functions for Data Formatting
Create helper functions for consistent data formatting:
// src/utils/formatters.js
export const formatCurrency = (value) => {
if (!value) return '$0';
const num = parseFloat(value);
if (num >= 1e9) return `$${(num / 1e9).toFixed(1)}B`;
if (num >= 1e6) return `$${(num / 1e6).toFixed(1)}M`;
if (num >= 1e3) return `$${(num / 1e3).toFixed(1)}K`;
return `$${num.toFixed(2)}`;
};
export const calculateAPR = (volume, tvl) => {
if (!volume || !tvl) return 0;
const dailyVolume = parseFloat(volume);
const totalValueLocked = parseFloat(tvl);
const feeRate = 0.003; // 0.3% fee
const dailyFees = dailyVolume * feeRate;
const dailyYield = dailyFees / totalValueLocked;
const annualizedYield = dailyYield * 365 * 100;
return annualizedYield;
};
export const formatPercentage = (value) => {
if (!value) return '0%';
return `${parseFloat(value).toFixed(2)}%`;
};
export const formatAddress = (address) => {
if (!address) return '';
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`;
};
Adding Performance Optimizations
Implement performance optimizations for better user experience:
// src/components/OptimizedDashboard.js
import React, { useMemo, useCallback } from 'react';
import { Grid, Container, Typography, Box } from '@mui/material';
import { useQuery } from '@apollo/client';
import { GET_UNISWAP_POOLS } from '../graphql/queries';
import PoolCard from './PoolCard';
import { formatCurrency } from '../utils/formatters';
const OptimizedDashboard = () => {
const { data, loading, error } = useQuery(GET_UNISWAP_POOLS, {
variables: {
first: 20,
orderBy: 'totalValueLockedUSD',
orderDirection: 'desc'
}
});
// Memoize expensive calculations
const poolsWithMetrics = useMemo(() => {
if (!data?.pools) return [];
return data.pools.map(pool => ({
...pool,
apr: calculateAPR(pool.volumeUSD, pool.totalValueLockedUSD),
formattedTVL: formatCurrency(pool.totalValueLockedUSD)
}));
}, [data?.pools]);
const totalTVL = useMemo(() => {
return poolsWithMetrics.reduce((sum, pool) =>
sum + parseFloat(pool.totalValueLockedUSD), 0
);
}, [poolsWithMetrics]);
const handlePoolClick = useCallback((pool) => {
// Handle pool selection
console.log('Selected pool:', pool);
}, []);
if (loading) return <Typography>Loading pools...</Typography>;
if (error) return <Typography>Error: {error.message}</Typography>;
return (
<Container maxWidth="xl">
<Box sx={{ my: 4 }}>
<Typography variant="h3" component="h1" gutterBottom>
Yield Farming Dashboard
</Typography>
<Typography variant="h6" sx={{ mb: 3 }}>
Total TVL: {formatCurrency(totalTVL)}
</Typography>
<Grid container spacing={2}>
{poolsWithMetrics.map((pool) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={pool.id}>
<PoolCard
pool={pool}
onClick={() => handlePoolClick(pool)}
/>
</Grid>
))}
</Grid>
</Box>
</Container>
);
};
export default OptimizedDashboard;
Testing Your Dashboard
Create tests for your components:
// src/components/__tests__/Dashboard.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import Dashboard from '../Dashboard';
import { GET_UNISWAP_POOLS } from '../../graphql/queries';
const mocks = [
{
request: {
query: GET_UNISWAP_POOLS,
variables: {
first: 20,
orderBy: 'totalValueLockedUSD',
orderDirection: 'desc'
}
},
result: {
data: {
pools: [
{
id: '0x1',
token0: { symbol: 'USDC', name: 'USD Coin' },
token1: { symbol: 'WETH', name: 'Wrapped Ether' },
feeTier: 3000,
totalValueLockedUSD: '1000000'
}
]
}
}
}
];
test('renders dashboard with pools', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<Dashboard />
</MockedProvider>
);
expect(screen.getByText('Loading pools...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Yield Farming Dashboard')).toBeInTheDocument();
expect(screen.getByText('USDC/WETH')).toBeInTheDocument();
});
});
Deploying Your Dashboard
Build for Production
npm run build
Environment Variables
Create a .env file for configuration:
REACT_APP_GRAPH_API_URL=https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3
REACT_APP_INFURA_PROJECT_ID=your_infura_project_id
REACT_APP_WALLET_CONNECT_PROJECT_ID=your_wallet_connect_project_id
Deploy to Vercel
npm install -g vercel
vercel --prod
Advanced Features to Add
Price Alerts
Implement notifications when yield rates change significantly:
// src/hooks/useYieldAlerts.js
import { useState, useEffect } from 'react';
const useYieldAlerts = (pools, threshold = 5) => {
const [alerts, setAlerts] = useState([]);
useEffect(() => {
const checkAlerts = () => {
pools.forEach(pool => {
const previousAPR = localStorage.getItem(`apr_${pool.id}`);
const currentAPR = calculateAPR(pool.volumeUSD, pool.totalValueLockedUSD);
if (previousAPR && Math.abs(currentAPR - parseFloat(previousAPR)) > threshold) {
setAlerts(prev => [...prev, {
poolId: pool.id,
message: `${pool.token0.symbol}/${pool.token1.symbol} APR changed by ${(currentAPR - previousAPR).toFixed(2)}%`,
timestamp: Date.now()
}]);
}
localStorage.setItem(`apr_${pool.id}`, currentAPR.toString());
});
};
checkAlerts();
}, [pools, threshold]);
return alerts;
};
export default useYieldAlerts;
Historical Analysis
Add deeper analytics with trend analysis:
// src/components/AnalyticsPanel.js
import React from 'react';
import { Paper, Typography, Grid, Box } from '@mui/material';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
const AnalyticsPanel = ({ poolData }) => {
const calculateTrend = (data) => {
if (data.length < 2) return 0;
const recent = data.slice(-7);
const older = data.slice(-14, -7);
const recentAvg = recent.reduce((sum, item) => sum + item.yield, 0) / recent.length;
const olderAvg = older.reduce((sum, item) => sum + item.yield, 0) / older.length;
return ((recentAvg - olderAvg) / olderAvg) * 100;
};
const trend = calculateTrend(poolData);
return (
<Paper sx={{ p: 3 }}>
<Typography variant="h6" sx={{ mb: 2 }}>
Pool Analytics
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Box>
<Typography variant="h4" color={trend > 0 ? 'success.main' : 'error.main'}>
{trend > 0 ? '+' : ''}{trend.toFixed(2)}%
</Typography>
<Typography variant="body2" color="text.secondary">
7-day trend
</Typography>
</Box>
</Grid>
<Grid item xs={12}>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={poolData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey="yield"
stroke="#2196f3"
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</Grid>
</Grid>
</Paper>
);
};
export default AnalyticsPanel;
Conclusion
You've built a comprehensive yield farming dashboard that combines React.js with The Graph Protocol. This dashboard provides real-time DeFi data, interactive visualizations, and portfolio tracking capabilities.
Key features implemented:
- Real-time pool data from The Graph Protocol
- Interactive charts with historical yield performance
- Wallet integration for personal portfolio tracking
- Performance optimizations for smooth user experience
- Responsive design that works on all devices
The dashboard gives you complete control over your DeFi investments by centralizing data from multiple protocols. You can extend this foundation by adding more DeFi protocols, implementing advanced analytics, or integrating with additional blockchain networks.
Start building your yield farming dashboard today and take control of your DeFi portfolio management.