How to Build Yield Farming Dashboard with React.js and The Graph Protocol

Learn to build a yield farming dashboard using React.js and The Graph. Get real-time DeFi data with GraphQL queries and interactive UI components.

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.