Skip to content

ceteio/next-layout-loader

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Next Layout Loader




File-system based nested layouts for next.js

Try it on Codesandbox




yarn add @ceteio/next-layout-loader

Usage

Add _layout.tsx* files in your pages/ directory:

pages
├── _app.tsx
├── _layout.tsx
├── index.tsx
└── dashboard
    ├── _layout.tsx
    └── user
        ├── _layout.tsx
        └── index.tsx

* (Supports .tsx, .ts, .jsx, .js, or any custom filename with the layoutFilenames option)

For example:

// pages/_layout.tsx
import { useState } from "react";

// children is the file-system based component as rendered by next.js
export default function Layout({ children }) {
  // State is maintained between client-side route changes!
  const [count, setCount] = useState(0);
  return (
    <div style={{ border: "1px solid gray", padding: "1rem" }}>
      <p>
        <code>pages/_layout</code>
        <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      </p>
      {children}
    </div>
  );
}

// To hide this layout component from the router / build pipeline
export const getStaticProps = async () => ({ notFound: true });

Next, add some one-time boilerplate to _app (powered by preval & codegen):

// pages/_app.jsx
const filename = preval`module.exports = __filename`;
const withLayoutLoader = codegen.require("@ceteio/next-layout-loader", filename);

// Automatically renders _layout files appropriate for the current route
export default withLayoutLoader(({ Component, pageProps }) => (
  <Component {...pageProps} />
));

Now load your pages to see the layouts automatically applied!

Setup

Install all the dependencies:

yarn add @ceteio/next-layout-loader
yarn add babel-plugin-codegen@4.1.5 babel-plugin-preval
yarn add patch-package postinstall-postinstall

The usage of preval & codegen necessitates using babel, and hence opting-out of swc (if you know how to do codegen in swc, please let me know in #1!). To ensure the layout files are loaded correctly, you must include the codegen and preval plugins:

.babelrc

{
  "presets": ["next/babel"],
  "plugins": ["codegen", "preval"]
}

A patch is necessary for babel-plugin-codegen to correctly import the @ceteio/next-layout-loader module:

package.json

{
  "scripts": {
    "postinstall": "patch-package"
  }
}

And create a new file patches/babel-plugin-codegen+4.1.5.patch:

diff --git a/node_modules/babel-plugin-codegen/dist/helpers.js b/node_modules/babel-plugin-codegen/dist/helpers.js
index e292c8a..472d128 100644
--- a/node_modules/babel-plugin-codegen/dist/helpers.js
+++ b/node_modules/babel-plugin-codegen/dist/helpers.js
@@ -99,9 +99,8 @@ function resolveModuleContents({
   filename,
   module
 }) {
-  const resolvedPath = _path.default.resolve(_path.default.dirname(filename), module);
-
-  const code = _fs.default.readFileSync(require.resolve(resolvedPath));
+  const resolvedPath = require.resolve(module, { paths: [_path.default.dirname(filename)] })
+  const code = _fs.default.readFileSync(resolvedPath);

   return {
     code,

Then re-run yarn.

Configuration

codegen.require("@ceteio/next-layout-loader", <filename>[, options])

<filename>

Absolute path to the current page file.

In the simplest case, this can be hard-coded, but wouldn't work on a different computer, or if you were to move your source files around. Instead, we use preval & __filename to automatically generate the correct path for us:

const filename = preval`module.exports = __filename`;
const withLayoutLoader = codegen.require("@ceteio/next-layout-loader", filename);

(NOTE: This must remain as 2 separate lines. If you know how to minimise this boilerplate, please see #2).

options

An object of further options to affect how the library loads layout files.

codegen.require("@ceteio/next-layout-loader", filename, {
  layoutFilenames
});

options.layoutFilenames

Default: ['_layout.tsx', '_layout.ts', '_layout.jsx', '_layout.js']

The possible variations of layout file names within pages/. Can be overridden to use any name or extension you like.

How it works

The easiest way to understand with an example:

pages
├── index.tsx
├── _app.tsx
├── _layout.tsx
└── dashboard
    ├── _layout.tsx
    └── user
        ├── index.tsx
        └── _layout.tsx

pages/_app.tsx:

const filename = preval`module.exports = __filename`;
const withLayoutLoader = codegen.require(
  "@ceteio/next-layout-loader",
  filename
);

// Automatically renders _layout files appropriate for the current route
export default withLayoutLoader(({ Component, pageProps }) => (
  <Component {...pageProps} />
));

pages/dashboard/user/index.tsx:

export default function User() {
  return <h1>Hello world</h1>;
}

next-layout-loader will transform the pages/app.tsx into:

import dynamic from "next/dynamic";
import { Fragment } from "react";

// A map of directories to their layout components (if they exist)
const layoutMap = {
  "/": __dynamic(() => import("./_layout.jsx")),
  dashboard: __dynamic(() => import("./dashboard/_layout.jsx")),
  "dashboard/user": __dynamic(() => import("./dashboard/user/_layout.jsx"))
};

const withLayoutLoader = wrappedFn => context => {
  const { pageProps, router } = context;

  const renderedComponent = wrappedFn(context);

  return ({ Component, pageProps, router }) => {
    const Layout1 = layoutMap["/"];
    const Layout2 = layoutMap["dashboard"];
    const Layout3 = layoutMap["dashboard/user"];

    return (
      <Layout1 {...pageProps}>
        <Layout2 {...pageProps}>
          <Layout3 {...pageProps}>
            {renderedComponent}
          </Layout3>
        </Layout2>
      </Layout1>
    );
  };
})();

export default withLayoutLoader(({ Component, pageProps }) => (
  <Component {...pageProps} />
));

(Note: The above is a simplification; the real code has some extra logic to handle all routes and their layouts)

Frequently Asked Questions

Why does this exist?

This library started as Proof Of Concept based on a discussion in the Next.js repo, but it turned out to work quite well and match my mental model of how nested layouts should work. So I turned it into a library that anyone can use.

Why is an extra layout being applied?

An extra layout component can be unexpectedly rendered when you have the following situation:

pages
├── _layout.tsx
├── user.tsx
└── user
    └── _layout.tsx

Visiting /user may will render both pages/_layout.tsx and pages/user/_layout.tsx. This may not be expected (the later is in a child directory after all!), and is due to a difference in the way Next.js handles rendering pages vs how @ceteio/next-layout-loader loads layouts.

To work around this, move pages/user.tsx to pages/user/index.tsx:

 pages
 ├── _layout.tsx
-├── user.tsx
 └── user
+    ├── index.tsx
     └── _layout.tsx