Using GitHub API to fetch and display a GitHub user profile
Tags: github, api, webdev, react
Published: 2023-01-11 03:09:25

Github API can be used to programmatically interact with GitHub. It gives you access to publicly available user-generated information (like public repositories, user profiles, etc.) and with an authenticated access token can even grant access to private information (my guess though, is that if you are here, you already knew all that).

We are creating an app to access publicly available information with the Github API and display it in react. We are essentially transforming information provided by the Github API's endpoints from the traditional JSON format into aesthetically-more-pleasing to-look-at format (you could almost say we're redesigning Github's user interface).

Here are the features we want to implement in this app:

- A user-friendly UI to display a GitHub user's profile.

- A proper SEO for each page in the app.

- An error boundary and a page to test the error boundary.

- Routing.

- Pagination for fetched results retrieved as lists.


## Requirements
Code editor — preferably Visual Studio Code
Node.js
Basic knowledge of React

The code for the app can be found in this [GitHub repository](https://github.com/FalanaTolu/AltSchool-Projects/tree/main/React%20Exam/1%20-%20GithubApp).
A live demonstration can be found [here](https://sparkling-starship-a8100b.netlify.app).

## Creating and setting up a React app
Type the following command in the terminal to create a new React app called "myapp":
`npx create-react-app myapp`

After that, all that is needed is to type the command to install the npm packages we will be using for the project:

`npm install react-router-dom framer-motion prop-types react-helmet-async react-icons react-loader-spinner`

`react-helmet-async` was installed to handle the SEO, `framer-motion` to animate page transitions(to which `prop-types` was a dependency), `react-router-dom` handled the routing of the application. `react-icons` provided the icons used in the app.
`react-loader-spinner` was for the loading component.

For the color scheme, I used [envato.com](https://www.envato.com/blog/color-scheme-trends-in-mobile-app-design/).

To run the react app, in the terminal type:
`cd myapp && npm start`

## Implementation
My final folder structure looked something like this:

![Final Folder Structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/33p9scoyhog2ca6g97ch.png)

The first thing I do is to create files for the main pages of the app and store them in a folder called `pages`. We have decided that the main pages should the home, about, profile and test pages.

The home page `Home.js` is the app's root page, it welcomes users to the app and redirects to the profile page and the about page. 

```react
import React from 'react';
import { motion as m } from 'framer-motion';
import { Link } from 'react-router-dom';
import { FaGithub } from 'react-icons/fa';
import { SEO, Navigation } from '../components';

const Home = () => {
  return (
    <m.section
      initial={{ y: '100%' }}
      animate={{ y: '0%' }}
      transition={{ duration: 1, ease: 'easeOut' }}
      exit={{ opacity: 1 }}
      className="home-section"
    >
      <SEO
        title="Home"
        name="Home Page"
        description="Home page for Github Profile App."
        type="article"
      />
      <Navigation />
      <article className="home-text">
        <div className="home-title-container">
          <m.h1
            initial={{ x: '-250%' }}
            animate={{ x: '0%' }}
            transition={{ delay: 0.5, duration: 2, ease: 'easeOut' }}
          >
            <span className="icon-container">
              <FaGithub />
            </span>{' '}
            GitHub Profile App
          </m.h1>
        </div>
        <p>
          This app was made to retrieve the details of the user's github account
          with the help of the Github API.
        </p>
        <div className="link">
          <Link to="/user">
            <button>Get Started</button>
          </Link>
          <Link to="/about">
            <button>Learn More</button>
          </Link>
        </div>
      </article>
    </m.section>
  );
};

export default Home;
```

The about page `About.js` gives random information about the app:
```react
import React from 'react';
import { motion as m } from 'framer-motion';
import { Link } from 'react-router-dom';
import { SEO, Navigation } from '../components';

const About = () => {
  return (
    <m.section
      initial={{ y: '100%' }}
      animate={{ y: '0%' }}
      transition={{ duration: 0.8, ease: 'easeOut' }}
      exit={{ opacity: 1 }}
      className="about-section"
    >
      <SEO
        title="About"
        name="About Page"
        description="About page for Github Profile App."
        type="Info Page"
      />
      <Navigation />
      <h1>About Page</h1>
      <p>
        This app displays the user's Github profile information. This was
        achieved by fetching data containing the details of the user's Github
        account from the Github API.
      </p>
      <p>
        The information displayed includes the user's repositories, the user's
        most recent activities, a snapshot of Github accounts following the user
        and accounts the user follows.
      </p>
      <p>
        Only information available publicly through the Github API was used.
      </p>
      <div className="link">
        <Link to="/">
          <button>Back Home</button>
        </Link>
      </div>
      <div className="about-image">
        <img
          src="https://github.githubassets.com/images/modules/site/home-campaign/astrocat.png?width=480&format=webpll"
          alt="astrocat"
        />
        <p>credit: www.github.com</p>
      </div>
    </m.section>
  );
};

export default About;
```
The test page `Test.js` is for the error boundary test. It has a button that throws an error so the error boundary can be seen in action(more on the error boundary later).

```react
import React, { useState } from 'react';
import { Navigation } from '../components';

export default function Test() {
  const [error, setError] = useState(false);

  if (error)
    throw new Error(
      'Oh! See! Should have warned you. This page tests the error boundary!'
    );

  return (
    <section>
      <Navigation />
      <div className="link">
        <h1>Hello!</h1>
        <button
          onClick={() => {
            setError(true);
          }}
        >
          Click Me!
        </button>
      </div>
    </section>
  );
}
```
The profile page `Profile.js` is the application's main page. 

```react
import React, { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { motion as m } from 'framer-motion';
import { Navigation, SEO, Loader, GetFetch, InputUser } from '../components';
import { ProfileView } from '../pages';
import { useEffect } from 'react';

const Profile = () => {
  const url = `https://api.github.com/users/${user}`;

  const { data, loading, error, fetchUsers } = GetFetch(url);

  useEffect (() => {fetchUsers()},[user])

  if (loading) {
    return <Loader />;
  }

  if (error) {
    return <h2>{`Fetch error: ${error.message}`}</h2>;
  }

  return (
    <m.section
      initial={{ x: '-50%' }}
      animate={{ x: '0%' }}
      transition={{ duration: 1, ease: 'easeOut' }}
      exit={{ opacity: 1 }}
      className="profile-section"
    >
      <SEO
        title="Profile"
        name="Profile Page"
        description="Github profile information is displayed here using the Github API"
        type="App"
      />
      <Navigation />
      <ProfileView data={data} />
      <Outlet context={data} />
    </m.section>
  );
};

export default Profile;
```
The profile page receives data fetched by a `GetFetch.js` component. This component uses an async function `fetchUsers` that is capable of accepting a `url` parameter to retrieve data from the Github users' endpoint.
The `GetFetch.js` component can be seen below:
```react
import { useState, useEffect } from 'react';

const GetFetch = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchUsers = async () => {
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP Error: ${response.status}`);
      }
      const result = await response.json();
      setData(result);
      setError(null);
    } catch (err) {
      setError(err.message);
      setData(null);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchUsers();
  }, []);

  return { data, loading, error, fetchUsers };
};
```
To improve readability, the data retrieved in the profile page is passed as a `data` prop to a `ProfileView.js` component.

We have also decided that this app should be able to show not just the user's general information, but also the user's repositories, a single repository, events in the last 30 days, the accounts following the user and the account the user follows. We are creating "sub-pages" for these. The "sub-pages" are created as nested routes in the app that can be accessed as outlets in the profile page. 
Data is passed along to these outlets in the form of outlet context(specifically so we can get the total number of items necessary for paginating these sub-pages).
So the `App.js` file should look like this:

```react
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import {
  Home,
  About,
  NotFound,
  Test,
  Profile,
  Repo,
  Activity,
  Following,
  Followers,
  SingleRepo,
} from './pages';
import './index.css';

export default function App() {
   return (
    <section>
        <Route path="/" element={<Home />} />
        <Route exact path="/user" element={<Profile />}>
          <Route exact path="/user/repo" element={<Repo />}>
            <Route exact path=":repoid" element={<SingleRepo />} />
          </Route>
          <Route exact path="/user/activity" element={<Activity />} />
          <Route exact path="/user/following" element={<Following />} />
          <Route exact path="/user/followers" element={<Followers />} />
        </Route>
        <Route path="about" element={<About />} />
        <Route path="test" element={<Test />} />
        <Route path="*" element={<NotFound />} />
    </section>
  );
}

```

The `ProfileView.js` component: 

```react
import React from 'react';
import { NavLink } from 'react-router-dom';
import { TfiNewWindow } from 'react-icons/tfi';
import { FaMapMarkerAlt } from 'react-icons/fa';

const ProfileView = ({ data }) => {
  return (
    <article className="profile-container">
      <h2 className="profile-title">Profile</h2>
      <div className="profile">
        <div className="profile-image-container">
          <img src={data.avatar_url} alt={data.name} />
          <div>
            <span className="icon-container">
              <FaMapMarkerAlt />
            </span>{' '}
            {data.location}
          </div>
        </div>
        <div className="profile-text">
          <h3>{data.name}</h3>
          <p>
            <span>Login name:</span> {data.login}
          </p>
          <p>
            <span>Bio:</span> {data.bio}
          </p>
          <p>
            <span>Email:</span> {data.email}
          </p>
          <p>
            <span>Twitter: </span> {data.twitter_username}
          </p>
          <p>
            <span>Joined:</span> {new Date(data.created_at).toLocaleString()}
          </p>
          <p>
            <span>Public Repos:</span> {data.public_repos} &#160;&#160;{' '}
            <span>Last Update:</span>{' '}
            {new Date(data.updated_at).toLocaleString()}
          </p>
          <p>
            <span>Followers:</span> {data.followers} &#160;&#160;{' '}
            <span>Following:</span> {data.following}
          </p>
          <p className="external-link">
            <a href={data.html_url} target="_blank" rel="noreferrer">
              View on Github <TfiNewWindow fill="rgb(145, 145, 145)" />
            </a>
          </p>
        </div>
      </div>
      <h3 className="link-to-outlet">
        <NavLink
          className={({ isActive }) => (isActive ? 'active-link' : '')}
          to="/user/repo"
        >
          Repositories
        </NavLink>
        <NavLink
          className={({ isActive }) => (isActive ? 'active-link' : '')}
          to="/user/activity"
        >
          Activity
        </NavLink>
        <NavLink
          className={({ isActive }) => (isActive ? 'active-link' : '')}
          to="/user/following"
        >
          Following
        </NavLink>
        <NavLink
          className={({ isActive }) => (isActive ? 'active-link' : '')}
          to="/user/followers"
        >
          Followers
        </NavLink>
      </h3>
    </article>
  );
};

export default ProfileView;
```

Okay, so we have our nested routes. Here is where we run into a bit of a problem though. The Github rest API can only return a maximum of 30 pages of data. To solve this, we create a new `GetFetchPages` component to create a concatenated list of fetched data using an async function `fetchPages`:

```react
const GetFetchPages = (baseUrl, totalNum, page, per_page) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const startIndex = page * per_page - per_page;
  const totalPages = Math.ceil(totalNum / per_page);

  const fetchPages = async (page) => {
    const fetchPromise = [];
    try {
      for (page = 1; page < totalPages + 1; page++) {
        const response = await fetch(
          `${baseUrl}?page=${page}&per_page=${per_page}`
        );
        if (!response.ok) {
          throw new Error(`HTTP Error: ${response.status}`);
        }
        fetchPromise.push(response);
      }
      const responses = await Promise.all(fetchPromise);
      const results = await Promise.all(
        responses.map((response) => response.json())
      );
      let dataList = [];
      results.forEach((result) => {
        dataList = dataList.concat(result);
      });
      setData(dataList);
      setError(null);
    } catch (err) {
      setError(err.message);
      setData(null);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchPages();
    // eslint-disable-next-line
  }, []);

  return { data, loading, error, startIndex, page, per_page, totalPages };
};
```

The data from the outlet context and that of the `GetFetchPages` is used to paginate the sub-pages like in `Followers.js`:

```react
import React, { useState } from 'react';
import { useOutletContext } from 'react-router-dom';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import { Loader, GetFetchPages } from '../components';

const Followers = () => {
  const user = useOutletContext();

  const [page, setPage] = useState(1);

  const totalNum = user.followers;
  const per_page = 10;


  const baseUrl = user.followers_url;

  const { data, loading, error, startIndex, totalPages } = GetFetchPages(
    baseUrl,
    totalNum,
    page,
    per_page
  );

  if (loading) {
    return <Loader />;
  }

  if (error) {
    return <h2>{`Fetch error: ${error.message}`}</h2>;
  }

  return (
    <>
    <div className="list follow">
      {data?.slice(startIndex, startIndex + per_page).map((followers) => {
        return (
          <article className="followers" key={followers.id}>
            <div>
              <div>
                <img src={followers.avatar_url} alt={followers.login} />
              </div>
              <p>
                <span>Login name:</span>{' '}
                <a href={followers.html_url}>{followers.login}</a>
              </p>
              <p className="external-link">
                <a href={followers.html_url} target="_blank"  rel="noreferrer">
                  View Profile
                </a>
              </p>
            </div>
          </article>
        );
      })}
    </div>
    <div className="page-nav">
        <p>
          Pages: {page} of {totalPages}{' '}
        </p>
        <button 
          onClick={() => setPage((prev) => Math.max(prev - 1, 0))}
          disabled={page <= 1}
          aria-disabled={page <= 1}
        >
          <FaChevronLeft />
        </button>
        {Array.from({ length: totalPages }, (value, index) => index + 1).map(
          (each, index) => (
            <button key={index} onClick={() => setPage(each)} id={each === page && "current"}>
              {each}
            </button>
          )
        )}
        <button
          onClick={() => setPage((prev) => prev + 1)}
          disabled={page >= totalPages}
          aria-disabled={page >= totalPages}
        >
          <FaChevronRight />
        </button>
      </div>
      </>
  );
};

export default Followers;
```
The error boundary is implemented as a class component in `ErrorBoundary.js`:

```react
import React, { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { error: '', errorInfo: '', hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({ errorInfo });
    console.log(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div style={{ padding: '20px' }}>
          <h1>Some error don happen.</h1>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            <p>{this.state.error && this.state.error.toString()}</p>
            <pre>{this.state.errorInfo.componentStack}</pre>
            {console.log(
              `Some error don happen: ${this.state.errorInfo.componentStack}`
            )}
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

```
The app is wrapped with the SEO(`HelmetProvider` from `react-helmet-async`) and error-boundary components in the `index.js`file:

```react
import React, { Suspense, StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter as Router } from "react-router-dom";
import ErrorBoundary from "./components/ErrorBoundary";
import { HelmetProvider } from "react-helmet-async";
import { Loader } from "./components";
import App from "./App";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <Router>
    <StrictMode>
      <ErrorBoundary>
        <HelmetProvider>
          <Suspense fallback={<Loader />}>
            <App />
          </Suspense>
        </HelmetProvider>
      </ErrorBoundary>
    </StrictMode>
  </Router>
);

```

## Wrapping Up
All features should work perfectly once implemented in the other subpages.

By the way, this was my first ever write-up and this app was part of an exam conducted by the AltSchool Africa School of Software Engineering. I would appreciate all comments and suggestions for improvement.

The code to my final solution, where I try to display each repo as a modal and implement a user input to change the app's user can be seen [here](https://github.com/FalanaTolu/AltSchool-Projects/tree/main/React%20Exam/1%20-%20GithubApp). 
You can also check out the three other questions and how I attempted to solve them [here](https://github.com/FalanaTolu/AltSchool-Projects/tree/main/React%20Exam).