npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ceteio/next-layout-loader

v2.0.1

Published

Automatic next.js layout component loader

Readme

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 _layout.tsx, _layout.ts, _layout.jsx, _layout.js, or any custom filename with the layoutFilenames option)

For example:

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

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, wrap your _app with layout loading logic (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 [email protected] 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