DEV Community

Cover image for React Static Basics
Robert Peterman
Robert Peterman

Posted on • Updated on

React Static Basics

Since I’m a big fan of React, I’m always looking for new React based tools to add to my tool belt. However, there’s been one area of the React ecosystem that I’ve been avoiding… static site generators. A while back I had looked at the documentation for Gatsby to see if it fit my use case for a freelance project. After looking through all the new concepts I would have to learn and getting the sense that it was probably overkill for what I needed to do, I opted for a basic WordPress site and moved on. Thankfully I was recently reintroduced to static site generators through React Static and have been really impressed with how easy it is to get up and running. Not to mention how fast it is.

What is React Static?

According to its GitHub repo, React Static is a progressive static site generator built with a focus on performance, flexibility, and developer experience. Many of its features were built to address the shortcomings of other static site generators like Gatsby and Next.js. Since I’ve never used either of these I can’t go into great detail about how all of these generators differ but at a high level React Static achieves the ease of use of Next.js while coming (somewhat) close to matching the performance of Gatsby. React Static makes sure that creating static sites doesn’t get tedious or overly complex and always maintains a create-react-app feel. These areas are where React Static differentiates itself from competitors.

Getting Started

The developer experience with React Static is one of it’s highlights. If you’ve used “create-react-app” before you’ll appreciate their “react-static create” which will setup a template for you with zero configuration. Run these commands to get started.

npm i -g react-static
react-static create

After running these commands you can choose from a number of different starter templates. For this article we will go with the basic template. After the template has been created for you, open up the new directory and open static.config.js.

Importing data

To summarize, this file's job is to match our project's routes with a page and the data needed by that page. Read through the comment code to get an idea of whats going on.

//static.config.js
export default {

  // resolves an array of route objects 
  getRoutes: async () => {

    // this is where you can make requests for data that will be needed for all
    // routes or multiple routes - values returned can then be reused in route objects below

    // starter template has a request to an endpoint that retrieves an array of fake blog posts
    const { data: posts } = await axios.get(
      "https://jsonplaceholder.typicode.com/posts"
    );

    return [
      // route object
      {
        // React Static looks for files in src/pages (see plugins below) and matches them to path
        path: "/blog",
        // function that returns data for this specific route
        getData: () => ({
          posts
        }),
        // an array of children routes
        // in this case we are mapping through the blog posts from the post variable above
        // and setting a custom route for each one based off their post id
        children: posts.map(post => ({
          path: `/post/${post.id}`,
          // location of template for child route
          template: "src/containers/Post",
          // passing the individual post data needed
          getData: () => ({
            post
          })
        }))
      },
    ];
  },
  // basic template default plugins
  plugins: [
    [
      require.resolve("react-static-plugin-source-filesystem"),
      {
        location: path.resolve("./src/pages")
      }
    ],
    require.resolve("react-static-plugin-reach-router"),
    require.resolve("react-static-plugin-sitemap")
  ]
};

You can then access this data inside the blog page like this.

// src/pages/blog
import React from 'react'
// provides the route's data
import { useRouteData } from 'react-static'
import { Link } from 'components/Router'

// making a ul of links to all of the individual blog posts
export default function Blog() {
  const { posts } = useRouteData()
  return ( 
    <div>
      <h1>It's blog time.</h1>
      <div>
        <a href="#bottom" id="top">
          Scroll to bottom!
        </a>
      </div>
      <br />
      All Posts:
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            {/* linking to individual post routes set by children property of /blog route */}
            <Link to={`/blog/post/${post.id}/`}>{post.title}</Link>
          </li>
        ))}
      </ul>
      <a href="#top" id="bottom">
        Scroll to top!
      </a>
    </div>
  )
}

That's all there is to it - you bring in data, feed it into your routes, and then use that data however you want.

CMS Integration

At some point you might want to hook up your React Static site to a CMS. Since the developer experience is a focus with React Static, we might as well use a CMS that takes this same approach – Netlify CMS. The Netlify CMS is git based so whenever your CMS is updated, the updated content is pushed to your static site’s GitHub repo which in turn triggers Netlify to rebuild the site (and the updated data being added to the appropriate routes through static.config.js).

To set this up, push your static site repo to GitHub and connect it to Netlify by clicking the “New site from Git” button inside your Netlify Dashboard.

Enter "npm run build" for the build command and "dist" for the root directory.

Then go to the Identity tab and select “Enable Identity Service”.

Under Registration preferences select Invite Only.

I’d suggest selecting a provider under “External Providers” for convenience. I’m using Google for this tutorial.

Select the identity tab towards the top of the page and click invite users – invite your personal gmail account to give yourself access. Finish the registration process through your email.

Scroll down to Services > Git Gateway, and click Enable Git Gateway.

Now go back to the root of your project directory and find your public folder – it should already have robots.txt inside. Add an admin and uploads folder here. Inside the newly created admin folder create two new files – config.yml and index.html.

Paste the following code into config.yml. This sets the structure of a collection labeled Test Collection.

backend:
  name: git-gateway
  branch: master # Branch to update (optional; defaults to master)

media_folder: "public/uploads" # Media files will be stored in the repo under public/uploads
public_folder: "/uploads" # Folder path where uploaded files will be accessed, relative to the base of the built site

collections:
  - name: "test-collection" # Used in routes, e.g., /admin/collections/test-collection
    label: "Test Collection" # Used in the UI
    folder: "src/test-collection" # The path to the folder where the documents are stored
    create: true # Allow users to create new documents in this collection
    slug: "{{year}}-{{month}}-{{day}}-{{slug}}" # Filename template, e.g., YYYY-MM-DD-title.md
    fields: # The fields for each document, usually in front matter. 
    # Remove any that aren't needed for posts
      - {label: "Layout", name: "layout", widget: "hidden", default: "blog"}
      - {label: "Title", name: "title", widget: "string"}
      - {label: "Publish Date", name: "date", widget: "datetime"}
      - {label: "Body", name: "body", widget: "markdown"}

Then paste this code into into the index.html file. This sets up the admin login modal + CMS at the /admin route of your site.

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Content Manager</title>
  <script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
</head>
<body>
  <!-- Include the script that builds the page and powers Netlify CMS -->
  <script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
  <script>
    if (window.netlifyIdentity) {
      window.netlifyIdentity.on("init", user => {
        if (!user) {
          window.netlifyIdentity.on("login", () => {
            document.location.href = "/admin/";
          });
        }
      });
    }
  </script>
</body>
</html>

Last but not least navigate back to static.config.js and add this code. Once we add a post to our "Test Collection" collection this function will read it from our repo and make it available to our route objects. You'll need to run npm install fs klaw gray-matter to download the dependencies.

//static.config.js

//...other imports

//for reading local files
import fs  from "fs";
import klaw from "klaw";
import matter from "gray-matter";

function getPosts() {
  const items = [];
  // Walk ("klaw") through posts directory and push file paths into items array //
  const getFiles = () =>
    new Promise(resolve => {
      // Check if test-collect directory exists //
      // This is the folder where your CMS collection we made earlier will store it's content. Creating a post inside this collection will add a "test-collection" directory to your repo for you.
      if (fs.existsSync("./src/test-collection")) {
        klaw("./src/test-collection")
          .on("data", item => {
            // Filter function to retrieve .md files //
            if (path.extname(item.path) === ".md") {
              // If markdown file, read contents //
              const data = fs.readFileSync(item.path, "utf8");
              // Convert to frontmatter object and markdown content //
              const dataObj = matter(data);
              // Create slug for URL //
              dataObj.data.slug = dataObj.data.title
                .toLowerCase()
                .replace(/ /g, "-")
                .replace(/[^\w-]+/g, "");
              // Remove unused key //
              delete dataObj.orig;
              // Push object into items array //
              items.push(dataObj);
            }
          })
          .on("error", e => {
            console.log(e);
          })
          .on("end", () => {
            // Resolve promise for async getRoutes request //
            // posts = items for below routes //
            resolve(items);
          });
      } else {
        // If src/posts directory doesn't exist, return items as empty array //
        resolve(items);
      }
    });
  return getFiles(); 
}

Add const test = await getPosts() inside getRoutes next to where we make the blog posts request. This test variable will now hold the values of any content we make in our collection.

Create a new route object and add it to the array of routes like so...

// add below /blog route object
      {
        path: "/test",
        getData: () => ({
          test
        }),
        children: test.map(post => ({
          // actual path will be /test/"whatever the post slug is"
          path: `/${post.data.slug}`,
          // location of template for child route
          template: "src/containers/Test-Post",
          // passing the individual post data needed
          getData: () => ({
            post
          })
        }))
      }

At this point we just need a page to display our content to make sure everything is working. To accomplish this quickly, just add a test.js file to src/pages and paste this code. We're creating a list of links to each test-collection post we've made.

// src/pages/test.js

import React from "react";
import { useRouteData } from "react-static";
import { Link } from 'components/Router'

export default function Test() {
  const { test } = useRouteData();
  return (
    <ul>
      {test.map((item, index)=>{
          return (
              <li key={index}>   
                <Link to={`/test/${item.data.slug}`}>{item.data.title}</Link>
              </li>
          )
      })}
    </ul>
  );
}

Then add in your new post template by creating Test-Post.js in the containers folder.

// src/containers/Test-Post.js

import React from 'react'
import { useRouteData } from 'react-static'
import { Link } from 'components/Router'
// very similar to Post.js component
export default function Post() {
  // get the post data
  const { post } = useRouteData()
  return (
    <div>
      <Link to="/test/">{'<'} Back</Link>
      <br />
      {/* print out what we want to display */}
      <h3>{post.data.title}</h3>
      <div>{post.content}</div>
    </div>
  )
}

Also add a nav link to the new page in src/App.js for convenience <Link to="/test">Test</Link>

After pushing your changes to your GitHub repo, Netlify will now rebuild the site with Netlify CMS included. With your live Netlify url open, add /admin to the root url of your site in the browser to login to Netlify CMS. Once you’re in, write up some content and publish it inside "Test Collection". Netlify will once again rebuild your site with the new content you’ve added. When the rebuild is done your test.js page that was created earlier should display all the links to your posts.

That’s all there is to it. You now have a lightning fast static site connected to a modern CMS with a fully automated deployment flow.

Final Repo

Note – if you see two 404 errors in the developer console when using the Netlify CMS you can safely ignore these. I confirmed with some Netlify Community members that these are expected errors and are caused when you do not use Git LFS / Netlify Large Media settings. This issue is actually marked as good first issue if anyone is interested - https://github.com/netlify/netlify-cms/issues/2158

Top comments (1)

Collapse
 
sometimescasey profile image
Casey Juanxi Li

Super helpful, thank you! I actually just replaced the provided axios call with a call to my Contentful API but this article was really helpful for understanding where the dummy blog posts in the example were coming from, because this wasn't obvious at all. TY!