/ React

4 Techniques for Structuring Next.js Applications

Next.js is an excellent tool. I see it as Ruby on Rails for React.

I have been using Next as starting point for React applications for about 3 years. Last December, me and my team were able to migrate Product Hunt to Next.

Here are some of the techniques I use to structure Next applications:

  1. Keep screens out of page folder
  2. Wrap Next's Link component
  3. Have a paths helper to generate links
  4. Build each page via a createPage helper

Technique 1: Keep screens out of the pages folder

Next uses a file-based router. It is lightweight and gives you a good overview of your pages. However, pages tend to have many "private" components, which you want to keep near the page code. Also, the URL structure of your application is often different than the domain structure.

Because of this, I use the following directory structure:

~/components/
~/screens/
~/pages/

pages is the Next router. Files there are just imports of components from screens.

// ~/pages/posts/[id].ts
import page from '~/screens/posts/show'

export default page;

screens is where the actual pages live. They are grouped by domain. Everything in this folder is considered "private" and can't be imported from the outside.

~/screens/posts/show/index.tsx - entry point of screen
~/screens/posts/show/Query.graphql - query to fetch the data
~/screens/posts/show/utils.ts - helper functions
~/screens/posts/show/PrivateComponent1.ts - some "private" component
~/screens/posts/show/PrivateComponent2.ts - other "private" component

All reusable components are in components. My definition of "reusable" is "used at least in two places".

Here you can read more about structuring components in general.

Technique 2: Wrap Next's Link component

Every routing library has a Link component. It is used instead of the <a> tag. When clicked, it changes the URL without a full page redirect, and then the routing handles loading and displaying the new page.

I always wrap the routing library Link with my own Link component.

Next's Link component interface is a bit different than that of the others, but it functions in the same way:

<Link href="/company">
  <a>Company</a>
</Link>

I don't like that there are two components here - it is too cumbersome for my taste. This alone is a reason enough to wrap it.

Wrapping it also makes it easier to migrate to something like Remix or Gatsby, if needed.

Here is my wrapped Link:

import * as React from 'react';
import Link from 'next/link';

// support both static and dynamic paths
type IPath = string | { as: string; href: string };

// allow this component to accept all properties of an "a" tag
interface IProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
  to: IPath;
  prefetch?: boolean
  // ... more props we want to pass to Next Link
}

// Overwrite, Next default prefetch
export default React.forwardRef(({ to, prefetch = false, ...props }: IProps, ref: any) => {
  if (typeof to === 'string') {
    return (
      <Link href={to} prefetch={prefetch}>
        <a {...props} ref={ref} />
      </Link>
    );
  }
	 
  return (
    <Link href={to.href} as={to.as} prefetch={prefetch}>
      <a {...props} ref={ref} />
    </Link>
  );
});

Notice how I have overwritten the default prefetch value. Having our Link component allows us to do things like this.

Having such a wrapper was very useful the two times Next changed how they handle dynamic routes.

People often use hardcoded links in React applications:

<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
<Link to={{ href: '/post/[id], as: '/post/1' }}>Post 1</Link>

This is brittle, not type-safe, and makes renaming URLs or changing URL structure hard.

I have solved this by having a paths.ts file that defines all routes in my apps:

export default {
  about: '/about',
  contact: '/contact',
  post: ({ id }: { id: string }) => { href: '/post/[id], as: `/post/${id}` }
}

This way, I can change the routes, and I’m protected from typos.

import paths from '~/paths';

<Link to={paths.about}>About</Link>
<Link to={paths.contact}>Contact</Link>
<Link to={paths.post(1)}>Post 1</Link>

Technique 4: createPage

Every page entry uses the createPage helper. It handles the basics each page requires - data fetching, error handling, SEO tags, permissions, layout, etc.

// components/Component/index.js
import QUERY from './Query';
import { SomePage as IData } from 'graph/SomePage';

// 1) Create page as a default export
// makes it easy to quickly see the page options
export default createPage<IData>({
  // 2) Lead with Query
  query: QUERY,
  queryVariable: ({ id }) => ({ id });

  // 3) What are the page requirements
  requireLogin: 'edit post',
  requireAdmin: true,
  requireFeature: 'new-edit-page',
  requirePermissions: ({ post }) => post.canEdit,

  // 4) Rest of the options
  layout: PostLayout,

  bodyClassName: 'white-background',

  setTags: ({ post }) => post.meta,
  setStructuredData: ({ post }) => post.structuredData,

  // 5) the Page's main component
  renderComponent({ data }) {
    // render the page
  }
});

Conclusion

I had used those techniques before Next existed. They help me to keep the whole system decoupled and make it obvious what is used where.

Because of this, my team and I migrated Product Hunt (a 7-year-old codebase) to Next in about a month.

I hope you find them useful as well. For any questions or comments, you can ping me on Twitter.