Day 14: (Finalize) List Pages on Next.js

#100DaysOfProjects

Today I continued the task I had started on yesterday: looking for the most performant way to load a directory or listing of mdx content.

Babel glob import limitations

So I tried a couple of variations of babel importers that would allow me to import a glob of files. My first couple of attempts didn't work great, but some of that was likely my own misconfiguration. Still: I ran into various success with different modules and how they offered to "glob" that data.

I like to choose modules with a high maintainability score, so after doing a little more searching I came upon a babel-macro that would allow exporting. Rather than over-rely on non-standard syntax, a macro seemed like a nice way of keeping complexity low(er) and hopefully more maintainable.

Importing a glob from a component

The macro offers a simple syncronous method that we can use to load a glob path, and using this from within a component module I was able to successfully grab an array of file information. Most specifically: I was looking for access to the mdx exports so that I could pull out specific data from frontMatter.

The component that I created initially was a simple module. It only exported a single component that showed <li>'s for each file (sans index) within a hardcoded directory.

// components/pages/OneHundredDays

import importAll from 'import-all.macro'
import { parse } from 'path'
import Link from 'next/link'

const files = importAll.sync('../pages/100days/*.mdx')
const postsArray = Object.keys(files)
    .filter((filePath) => {
        return !filePath.includes('index')
    })
    .map((fullPath) => {
        const pathParts = parse(fullPath)
        return pathParts.name
    })

const OneHundredDays = () => {
    return (
        <ul>
            {postsArray.map((postName) => (
                <li>
                    <Link href={`/100days/${postName}`}>{postName}</Link>
                </li>
            ))}
        </ul>
    )
}

export default OneHundredDays

This technically works! I could import this component and render a list of pages with links! Job done?

Nope. Not so fast. I'm not sure if it's obvious from the example code above, but this code will actually import the ENTIRE module for each file, and they are ALL included as part of the postsArray that the component exports. If I wanted to simply access front matter, I've now included everything, and any page that includes this component will include the weight of ALL linked pages. This might not be a huge issue now, but as the site grows in complexity this is growing weight added to every page that is completely unnecessary.

Example Build Results

Page                                         Size     First Load JS
┌ ○ /                                        1.27 kB        32.6 kB
├   /_app                                    0 B            31.4 kB
├ ○ /100days                                 161 B          51.6 kB
├ ○ /100days/day02                           1.71 kB        36.7 kB
├ ○ /100days/day03                           1.15 kB        36.1 kB
├ ○ /100days/day04                           608 B          35.6 kB
├ ○ /100days/day07                           1.18 kB        36.1 kB
├ ○ /100days/day08                           1.01 kB          36 kB
├ ○ /100days/day09                           1.85 kB        36.8 kB
├ ○ /100days/day10                           2.21 kB        37.2 kB
├ ○ /100days/day11                           2.3 kB         37.3 kB
├ ○ /100days/day12                           1.43 kB        36.4 kB
├ ○ /100days/day13                           167 B          51.6 kB
├ ● /100days/day14                           3.21 kB        38.2 kB
├ ● /100days/list-of-posts                   1.97 kB        33.3 kB
├ ○ /404                                     3.43 kB        34.8 kB
├ ○ /collab-class                            543 B          35.5 kB
├ ○ /digital-garden                          1.65 kB          33 kB
└ ○ /whois                                   946 B          35.9 kB
+ First Load JS shared by all                31.4 kB
  ├ chunks/b468e5533fe7b209d92dca.72a9d7.js  9.66 kB
  ├ chunks/commons.fad11a.js                 7.65 kB
  ├ chunks/main.3ff882.js                    7.1 kB
  ├ chunks/pages/_app.284487.js              6.22 kB
  ├ chunks/webpack.ccf5ab.js                 751 B
  └ css/252ec897cdb7ebc39122.css             968 B

Here's a few notes from the build results that illustrates some of the problem. Note the template for day13 where I've incluced the <OneHundredDays /> list component and that the first load js is much higher than the surrounding modules. We want to be careful here, because this isn't just one time bloat: as the list of pages available grows we start to add more and more additional content that is unnecessary to that component.

Next.js already offers an api for pages to load props from data supplied either at build or render time (via getStaticProps and getServerSideProps). A better approach would be to use these api's to load a more focused subset of data on a page by page basis.

In the example above: day14 and list-of-posts both include the data via getStaticProps which allows us to pass ONLY what is needed into the individual page templates.

getStaticProps with MDX

So one useful element of next pages is being able to feed in props via a couple of additional module exports. When you render pages with mdx it's not as obvious how you would hook into using props like this, but it's actually really useful still for loading data ahead of time.

export { getStaticProps } from 'components/getStaticProps/OneHundredDays'

My next site uses absolute imports (and the baseUrl property in tsconfig) which allows me to put all of my components into the /components directory and import them (or any other module) as though it was from that base dir. I then create some helper modules in components/getStaticProps that export const getStaticProps = and follow the standard nextjs pattern with that data loading function. By then exporting the function from any of the mdx pages, I can utilize data-pre-loading at build time.

I've included this in the head of a couple of pages so that I can load the list of posts via static props and not have to worry about extra weight landing in the pages. The data is loaded via props and therefore available to components within the page that want to render based on it (but luckily avoiding the full weight of an entire map of posts).

Connecting static props to components

So now that we have the data fed into the page via props, what can we do to actually render that data in a nice component within the page itself? Props are one obvious route to pass data into a component that work easily enough.

<PageList postsArray={props.postsArray} />

Which results in the following:

while keeping our overall page bundle size as minimal as possible, only exporting the content that we actually need (not the entire contents of all the other posts we haven't done anything with).

Final Build Results

Page                                         Size     First Load JS
┌ ○ /                                        1.27 kB        32.6 kB
├   /_app                                    0 B            31.4 kB
├ ○ /100days                                 163 B          53.4 kB
├ ○ /100days/day02                           1.71 kB        36.7 kB
├ ○ /100days/day03                           1.15 kB        36.1 kB
├ ○ /100days/day04                           609 B          35.6 kB
├ ○ /100days/day07                           1.18 kB        36.1 kB
├ ○ /100days/day08                           1.01 kB          36 kB
├ ○ /100days/day09                           1.85 kB        36.8 kB
├ ○ /100days/day10                           2.21 kB        37.2 kB
├ ○ /100days/day11                           2.3 kB         37.3 kB
├ ○ /100days/day12                           1.43 kB        36.4 kB
├ ○ /100days/day13                           169 B          53.4 kB
├ ● /100days/day14                           167 B          40.4 kB
├ ● /100days/list-of-posts                   1.97 kB        33.3 kB
├ ○ /404                                     3.43 kB        34.8 kB
├ ○ /collab-class                            543 B          35.5 kB
├ ○ /digital-garden                          1.65 kB          33 kB
└ ○ /whois                                   946 B          35.9 kB
+ First Load JS shared by all                31.4 kB
  ├ chunks/commons.fad11a.js                 7.65 kB
  ├ chunks/ea4a7f496fc3a519efc7cb.72a9d7.js  9.66 kB
  ├ chunks/main.875c4c.js                    7.1 kB
  ├ chunks/pages/_app.3dd0d8.js              6.22 kB
  ├ chunks/webpack.ccf5ab.js                 751 B
  └ css/252ec897cdb7ebc39122.css             968 B

We'll note here that the page day14 has a listing of all the same posts that we include on the 100days and 100days/day13 templates. But because we're smarter about including these props as part of the static generation (and only including file names that exist), we keep the overall page and it's dynamic components still as light as possible.

Do you want to explore?

To make it easier on myself to check this revision out in the future, I'm going to commit my work in progress code for the day. All of this will be available on the main branch for today's commit. In the future I'll clean up more of this, but I like the idea of keeping the proof of concept for today intact.

Today's Clippings

  • Switched to a new clippings component. lower-romans list-style-type ftw.
  • Tried rendering mdx content inside of a component (the new clippings component) and it works just dandy!
  • Wrapped up my tests from yesterday with a couple of data loading strategies that I feel okay about (documented above)
  • Discovered "Reflow" plugin for VSC. Now that I'm doing more editing of markdown directly, having some text flow options that hook in with my prettier config is extremely useful...

posted: 2020-08-29