DEV Community

Cover image for How to Add GitHub Contribution Stats to Your React App
Seif
Seif

Posted on

1

How to Add GitHub Contribution Stats to Your React App

Want to showcase your GitHub activity in your React portfolio? In this tutorial, I'll show you how to create a React component that displays your total GitHub contributions using GitHub's GraphQL API, complete with efficient caching. Let's build something cool! 🚀

What We'll Build

We'll create a React component that:

  • Fetches your GitHub contributions from 2020 to present
  • Includes both public and private contributions
  • Implements client-side caching to optimize performance
  • Shows a loading state while fetching
  • Handles errors gracefully

Prerequisites

Before we start, you'll need:

  1. A GitHub Personal Access Token (with read:user scope)
  2. A React project set up (using Create React App, Next.js, or your preferred setup)
  3. Basic knowledge of React hooks and async operations

Step 1: Setting Up the GitHub Token

First, create a .env file in your project root and add your GitHub token:

NEXT_PUBLIC_GITHUB_TOKEN=your_github_token_here
Enter fullscreen mode Exit fullscreen mode

Step 2: Creating the Data Fetching Utility

Create a new file called githubApi.js:

export async function fetchGithubCommits(username) {
  const GITHUB_TOKEN = process.env.NEXT_PUBLIC_GITHUB_TOKEN;
  const CACHE_KEY = `github-commits-${username}`;
  const CACHE_TTL = 3600; // 1 hour in seconds

  if (!GITHUB_TOKEN) {
    console.error("No GitHub token found!");
    throw new Error("GitHub token is required");
  }

  const cachedData = getCachedData(CACHE_KEY);
  if (cachedData) {
    return cachedData.value;
  }

  try {
    const currentYear = new Date().getFullYear();
    const startYear = 2020;
    let totalCommits = 0;

    for (let year = startYear; year <= currentYear; year++) {
      const query = `
        query($username: String!, $from: DateTime!, $to: DateTime!) {
          user(login: $username) {
            contributionsCollection(from: $from, to: $to) {
              totalCommitContributions
              restrictedContributionsCount
            }
          }
        }
      `;

      const response = await fetch("https://api.github.com/graphql", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${GITHUB_TOKEN}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          query,
          variables: {
            username,
            from: `${year}-01-01T00:00:00Z`,
            to: `${year}-12-31T23:59:59Z`,
          },
        }),
      });

      const data = await response.json();
      if (data.errors) {
        throw new Error(data.errors[0].message);
      }

      const yearCommits =
        (data.data?.user?.contributionsCollection?.totalCommitContributions || 0) +
        (data.data?.user?.contributionsCollection?.restrictedContributionsCount || 0);

      totalCommits += yearCommits;
    }

    setCachedData(CACHE_KEY, totalCommits, CACHE_TTL);
    return totalCommits;
  } catch (error) {
    console.error("Error fetching GitHub commits:", error);
    throw error;
  }
}

function setCachedData(key, value, ttl) {
  const item = {
    value,
    timestamp: Date.now(),
    ttl: ttl * 1000,
  };
  localStorage.setItem(key, JSON.stringify(item));
}

function getCachedData(key) {
  try {
    const item = localStorage.getItem(key);
    if (!item) return null;

    const parsedItem = JSON.parse(item);
    const now = Date.now();

    if (now - parsedItem.timestamp > parsedItem.ttl) {
      localStorage.removeItem(key);
      return null;
    }

    return parsedItem;
  } catch {
    return null;
  }
}

export function invalidateCommitsCache(username) {
  localStorage.removeItem(`github-commits-${username}`);
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Creating the React Component

Create a new file called GitHubStats.js:

import React, { useState, useEffect } from 'react';
import { fetchGithubCommits } from './githubApi';

const GitHubStats = ({ username }) => {
  const [commits, setCommits] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchStats = async () => {
      try {
        setLoading(true);
        setError(null);
        const totalCommits = await fetchGithubCommits(username);
        setCommits(totalCommits);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchStats();
  }, [username]);

  if (loading) {
    return <div>Loading GitHub stats...</div>;
  }

  if (error) {
    return <div>Error: {error}</div>;
  }

  return (
    <div className="github-stats">
      <h2>GitHub Contributions</h2>
      <p>Total commits since 2020: {commits.toLocaleString()}</p>
    </div>
  );
};

export default GitHubStats;
Enter fullscreen mode Exit fullscreen mode

Step 4: Adding Styles

Let's add some basic styling. Create GitHubStats.css:

.github-stats {
  padding: 20px;
  border-radius: 8px;
  background-color: #f6f8fa;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  margin: 20px 0;
}

.github-stats h2 {
  margin: 0 0 15px 0;
  color: #24292e;
}

.github-stats p {
  font-size: 1.2em;
  color: #586069;
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Using the Component

Now you can use the component in your app:

import GitHubStats from './GitHubStats';

function App() {
  return (
    <div className="App">
      <h1>My Developer Portfolio</h1>
      <GitHubStats username="your-github-username" />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Making it Better: Advanced Features

1. Adding a Refresh Button

Update GitHubStats.js to include a manual refresh option:

import React, { useState, useEffect } from 'react';
import { fetchGithubCommits, invalidateCommitsCache } from './githubApi';

const GitHubStats = ({ username }) => {
  // ... previous state declarations ...

  const handleRefresh = async () => {
    invalidateCommitsCache(username);
    await fetchStats();
  };

  return (
    <div className="github-stats">
      <h2>GitHub Contributions</h2>
      <p>Total commits since 2020: {commits.toLocaleString()}</p>
      <button onClick={handleRefresh} disabled={loading}>
        {loading ? 'Refreshing...' : 'Refresh Stats'}
      </button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

2. Adding Year-by-Year Breakdown

We can modify the component to show contributions per year:

const GitHubStats = ({ username }) => {
  const [yearlyStats, setYearlyStats] = useState({});
  // ... other state declarations ...

  const fetchYearlyStats = async () => {
    try {
      setLoading(true);
      setError(null);

      const currentYear = new Date().getFullYear();
      const stats = {};

      for (let year = 2020; year <= currentYear; year++) {
        const commits = await fetchGithubCommits(username, year);
        stats[year] = commits;
      }

      setYearlyStats(stats);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="github-stats">
      <h2>GitHub Contributions</h2>
      {Object.entries(yearlyStats).map(([year, count]) => (
        <p key={year}>
          {year}: {count.toLocaleString()} commits
        </p>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Performance Tips

  1. Caching Strategy: The current implementation caches data for an hour. Adjust the CACHE_TTL based on your needs.
  2. Error Boundaries: Consider wrapping the component in an Error Boundary to handle unexpected errors gracefully.
  3. Loading States: Add a skeleton loader instead of a simple "Loading..." text for better UX.

Common Issues and Solutions

  1. CORS Issues: Make sure you're using the correct GitHub API endpoint and headers.
  2. Token Permissions: Ensure your GitHub token has the required permissions.
  3. Rate Limiting: Handle GitHub's API rate limits by checking the remaining rate limit in the response headers.

You now have a fully functional GitHub stats component in your React app! This implementation provides a solid foundation that you can build upon. Some ideas for enhancement:

  • Add more GitHub statistics (like stars, PRs, issues)
  • Create visual representations using charts
  • Add animations for loading and updates
  • Implement more detailed error handling

Remember to keep your GitHub token secure and never commit it to your repository. Happy coding! 🎉

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

đź‘‹ Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay