I wanted to display some code from one of my GitHub repos in a React app, and I wanted it to update automatically on the site when I pushed a new version.
I wanted this for a small component library I was making for myself, so I could see the latest version of the component from the repo. To accomplish this I used the GitHub API and the Prism syntax highlighting package.
- Add the GitHub client package and Prism for syntax highlighting.
I also added the axios library for making API calls.
npm i @octokit/rest react-prism axios
2. Authenticate to GitHub from your React app.
- Create a personal access token here to authenticate to GitHub from the browser.
- You must be logged in to access the token. This will generate an Oauth token in the form of a string to use. Copy it because it will only be accessible once.
- I created a JS module called octokit.js and imported Octokit.
- Create a new GH instance. The only required params are the Oauth token for
auth
and auserAgent
.
> octokit.js
import { Octokit } from "@octokit/rest" export const octokit = new Octokit({
auth: "your token string here",
userAgent: 'skylight v1'
});
4. If you’re going to publish your project, you should hide the token in an .env
file.
- Create .env file at project root. In the command line at your project root, write
touch .env
. - Write
nano .env
- In the .env file, create a variable called
REACT_APP_GH
and assign the value to your GitHub token.
The variable name is not important, but it must start with
REACT_APP
to be used without external packages in create-react-app boilerplates. If you aren’t using a create-react-app boilerplate, see these instructions for using environmental variables in React.
In the octokit.js module, replace the token string with the hidden variable.
import { Octokit } from "@octokit/rest"export const octokit = new Octokit({
auth: process.env.REACT_APP_GH,
userAgent: 'skylight v1'
});
Remember to put the .env file in the .gitignore list if pushing to a public repo!!!
5. Call the GitHub API
- I called the endpoint from a custom hook I made called useOctokit.
> useOctokit.js
import { useState, useEffect } from 'react';
import { octokit } from '../utils/octokit';export default function useOctokit(path) {
const [code, setCode] = useState(null);
useEffect(() => {
async function onLoad() {
await octokit.request(
'GET /repos/{owner}/{repo}/contents/{path}', {
owner: 'jawblia',
repo: 'components',
path: `src/lib/components/${path}`
}).then(res => console.log(res)
}).catch(err => console.log(err));
}onLoad();},[])return {
code
}
};
The GitHub API requires filling out the fields for owner (the GH username that owns the repo), the repo name, and the path to the file you’re trying to render. I passed the path as a variable, because I wanted to render a different files depending on the page I’m on, but this could easily be hardcoded as well.
I believe the GH API does do rate limiting to 5,000 calls per hour with this approach, so this approach is not usable for sites with lots of traffic.
Console.logging the response, we see the successful call in the browser.
console.log(res)
:
Lots of information here. The URL from Github, the name of the file, the path. One thing notably missing (at first glance) is the actual code. It’s in the response, in the content
parameter. Github encodes all content, with base64, which is a format for transferring binary files to text strings so they can be transferred online. More about it here.
console.log(res.data.content):
aW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0JzsgCmltcG9ydCBGbGV4IGZyb20g
Jy4vbGF5b3V0L0ZsZXgnOwoKY29uc3QgQmFkZ2UgPSAoeyB2YWx1ZSB9KSA9
PiB7CiAgICByZXR1cm4gKAogICAgICAgICAgICA8RmxleCBjZW50ZXIgbWlk
ZGxlIGNsYXNzTmFtZT17YGJhZGdlICR7IXZhbHVlID8gJ2JhZGdlLS1ub25l
JyA6Jyd9IGB9PgogICAgICAgICAgICAgICAgPGg0IGNsYXNzTmFtZT0iaGVh
dnkiPnt2YWx1ZSB8fCAwfTwvaDQ+CiAgICAgICAgICAgIDwvRmxleD4KICAg
ICk7Cn0KIApleHBvcnQgZGVmYXVsdCBCYWRnZTs=
6. Decode the base-64 content from the response:
- There is a built-in JS method, atob, that takes a base64-encoded string as an argument and returns the decoded version in plaintext. (btoa does the opposite, encoding strings in base64.)
import { useState, useEffect } from 'react';
import { octokit } from '../utils/octokit';export default function useOctokit(path) {
const [code, setCode] = useState(null);
useEffect(() => {
async function onLoad() {
await octokit.request(
'GET /repos/{owner}/{repo}/contents/{path}', {
owner: 'jawblia',
repo: 'components',
path: `src/lib/components/${path}`
}).then(res => {
const encoded = res.data.content;
const decoded = atob(encoded);
setCode(decoded);
}).catch(err => console.log(err));
}onLoad();},[])return {
code
}
};
- Set the decoded string as the state,
code
. - Return
code
from the hook.
7. Pass the code and language to a syntax highlighting component.
- Return the plaintext code stored in state.
import React, { useState } from 'react';
import useOctokit from '../hooks/useOctokit';
import Codeblock from '../data/raw/Codeblock';const CodeDetails = (path) => {
const { code } = useOctokit(path); return <Codeblock code={code} lang='js'/>;
}export default CodeDetails;
8. Highlight the code and display it
- In a component, import Prism and use the
highlightAll()
method with useEffect to highlight the syntax
import React, { useEffect } from 'react';
import Prism from 'prismjs';
import './_styles/assets/prism.css';const Codeblock = ({ code, lang }) => { useEffect(() => {
Prism.highlightAll();
}, [code]);return (<div className="codeblock">
<pre className="line-numbers">
<code className={`language-${lang}`}>
{code}
</code>
</pre>
</div>
);
}export default Codeblock;
This is how it looks!
I also made a small switch button to toggle between React and SCSS. Full code for the toggle here.
That’s all.
References:
- Base64 can be decoded here: https://www.base64decode.org/, but we also decode it using the atob method.
- More about base64.
- https://docs.github.com/en/rest — GitHub API docs