April 3rd, 2019
Gatsby is a static site generator built on top of React components that uses GraphQL to pull in data from anywhere.
When I first read about Gatsby that description didn't really help me grasp what Gatsby was all about. An alternative way to describe it is:
Gatsby is the Jekyll killer.
Like Jekyll, Gatsby is a static site generator. That means that the Gatsby build tool will read your source files and your configs, and produce a static bundle of HTML/JS/CSS that you can deploy without a backend.
A common example is a blog: Rather than host your blog on Wordpress, where every page will have to be generated based on database models translated into a view and eventually pushed to the frontend via an HTML template, a static site generator will read all of your posts ahead of time and use templates to pre-generate the pages for you.
Unlike Jekyll, however, Gatsby can read data from practically anywhere. To generate pages with Jekyll, your data have to be formatted as markdown files; to generate pages with Gatbsy, your data can come from anywhere: CSV files, markdown files, Google Pages, even a Postgres database.
There are two key technologies that underpin Gatsby's ability to template data from anywhere: React components and GraphQL queries. Turns out that the basics of both of them are pretty easy to understand. I'll review each in turn.
Gatsby uses React components for templating data. This has the really nice property that it means you can make use of the entire React ecosystem for your app. It also has the downside that you have to understand components in order to use it.
What's a React component? At the most basic level, it's a composible piece of coupled HTML/CSS/JS. Here's a simple example:
// src/components/header.js
import React from "react"
export default props => <h1>{props.headerText}</h1>
// src/pages/about.js
import React from "react"
import Header from "../components/header"
export default () => (
<div style={{ color: `teal` }}>
<Header headerText="About Gatsby" />
<p>Such wow. Very React.</p>
</div>
)
At the most basic level, you can think of the role that components play in Gatsby as replacing Jekyll's Liquid templates, or Django's templating engine (like Jinja).
Here's what a component looks like that pulls data from a Postgres database and templates it:
const NetworkListPage = ({ data }) => (
<div className="row header">
<div className="card networks" style={{width: '100%'}}>
<h2><i className="fas fa-globe"></i> Network List</h2>
<p><br /></p>
<table className="table table-striped table-hover compact responsive" id="network-list">
<thead>
<tr>
<th scope="col"><p>Network</p></th>
<th scope="col"><p>Schools</p></th>
<th scope="col"><p>% Low Income</p></th>
<th scope="col"><p>% Asian</p></th>
<th scope="col"><p>% Black</p></th>
<th scope="col"><p>% Hispanic</p></th>
<th scope="col"><p>% White</p></th>
</tr>
</thead>
<tbody>
{data.cps.cps_app_network.map((network) => (
<tr>
<td><p><Link to={`/network/${network.slug}`}>{network.name}</Link></p></td>
<td><p>{network.school_set.count}</p></td>
<td><p>{network.fr_lunch}</p></td>
<td><p>{network.demo_asian}</p></td>
<td><p>{network.demo_aa}</p></td>
<td><p>{network.demo_hispanic}</p></td>
<td><p>{network.demo_white}</p></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
For contrast, here's the same code templated using Django templates:
<div class="row header">
<div class="card networks" style="width: 100%">
<h2><i class="fas fa-globe"></i> Network List</h2>
<p><br /></p>
<table class="table table-striped table-hover compact responsive" id="network-list">
<thead>
<tr>
<th scope="col"><p>Network</p></th>
<th scope="col"><p>Schools</p></th>
<th scope="col"><p>% Low Income</p></th>
<th scope="col"><p>% Asian</p></th>
<th scope="col"><p>% Black</p></th>
<th scope="col"><p>% Hispanic</p></th>
<th scope="col"><p>% White</p></th>
</tr>
</thead>
<tbody>
{% for network in networks %}
<tr>
<td><p><a href="/network/{{ network.slug }}">{{ network.name }}</a></p></td>
<td><p>{{ network.school_set.count }}</p></td>
<td><p>{{ network|avg_demo:"fr_lunch"|floatformat}}%</p></td>
<td><p>{{ network|avg_demo:"demo_asian"|floatformat}}%</p></td>
<td><p>{{ network|avg_demo:"demo_aa"|floatformat}}%</p></td>
<td><p>{{ network|avg_demo:"demo_hispanic"|floatformat}}%</p></td>
<td><p>{{ network|avg_demo:"demo_white"|floatformat}}%</p></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
The main difference here is that where Django needs server-side view logic to
create the networks
list, Gatsby serializes it directly to JSON and templates
it using a JavaScript map
operation.
So if Gatsby doesn't use view logic, how does it serialize objects to JSON for templating?
The answer is that Gatsby uses GraphQL as a source-agnostic data layer. What this means is that as long as you can write a plugin to make data available via GraphQL, Gatsby can use GraphQL queries to retrieve data for templating.
Let's take a look at the GraphQL query for the school networks example above:
const networkQuery = graphql`
query {
cps {
cps_app_network {
name
slug
fr_lunch
demo_asian
demo_aa
demo_hispanic
demo_white
cps_app_school {
school_id
}
}
}
}
`
This query works because we have a Hasura server sitting on top of our local
Postgres database, turning GraphQL queries into SQL queries and serializing
JSON for us. Notice how Hasura automatically figures out the reverse foreign-key
relationship between schools
and networks
and autopopulates the cps_app_school
object for us.
The reliance on GraphQL can sometimes feel clunky. Here's a not-so-nice example, showing how Gatsby pulls in and templates images:
const Header = ({ siteTitle }) => (
<StaticQuery
query={graphql`
query {
logo: file(relativePath: { eq: "ssce_logo.png" }) {
childImageSharp {
fixed(height: 60) {
...GatsbyImageSharpFixed
}
}
}
}
`}
render={data => (
<nav class="navbar navbar-expand-md navbar-light">
<div class="navbar-left">
<Link to="/">
<Img fixed={data.logo.childImageSharp.fixed} />
</Link>
</div>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse d-flex flex-row-reverse" id="navbarSupportedContent">
<NavItem href="/about" icon="question-circle" title="About" />
<NavItem href="/networks" icon="address-card" title="Network List" />
<NavItem href="/" icon="search" title="School Search" />
</div>
</nav>
)}
/>
Note that you can see components in action above in our use of NavItem
. Here's
the definition of the NavItem
component:
const NavItem = props => (
<div className="nav-item" style={{ fontSize: '15px', margin: '15px' }}>
<p style={{ textAlign: 'left', marginTop: '15px' }}>
<Link to={props.href}>
<i className={`fas fa-${props.icon}`}></i>
<strong>{props.title}</strong>
</Link>
</p>
</div>
)
The upshot is that all data -- image, CSV, markdown, whatever -- is accessible in Gatsby via GraphQL queries that populate React components. There's some necessary complexity here, but the implications are powerful: Gatsby can make a static site out of basically any data.
Gatsby's a great fit for static apps with lots of data that don't require any dynamic server-side functionality like authentication or user storage. While there are options for accomplishing these features in entirely client-side apps (Netlify has some cool features for this) I don't yet have enough confidence in any of them to recommend we use them.
When I thought about this further, I realized that this description fit a good number of DataMade projects I'd worked on, including:
- Neighborhood Opportunity Fund
- Various IHS projects
- Where to Buy
- And now, LISC CNDA
- Gatsby tutorial
- My branch of CPS SSCE using Gatsby
- Intro to React components
- How to GraphQL, Gatsby's recommended GraphQL tutorial