https://github.com/ceteio/next-layout-loader
File-system based nested layouts for next.js
https://github.com/ceteio/next-layout-loader
layout nextjs react
Last synced: 11 months ago
JSON representation
File-system based nested layouts for next.js
- Host: GitHub
- URL: https://github.com/ceteio/next-layout-loader
- Owner: ceteio
- License: mit
- Created: 2021-12-22T04:46:25.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2022-05-04T02:27:54.000Z (about 4 years ago)
- Last Synced: 2024-11-26T13:20:55.257Z (over 1 year ago)
- Topics: layout, nextjs, react
- Language: JavaScript
- Homepage:
- Size: 111 KB
- Stars: 29
- Watchers: 1
- Forks: 1
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
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 `.tsx`, `.ts`, `.jsx`, `.js`, or [any
custom filename with the `layoutFilenames` option](#optionslayoutfilenames))_
For example:
```javascript
// 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 (
pages/_layout
setCount(count + 1)}>Count: {count}
{children}
);
}
// 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`](https://github.com/kentcdodds/babel-plugin-preval) &
[`codegen`](https://github.com/kentcdodds/babel-plugin-codegen)_):
```javascript
// 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 }) => (
));
```
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`](https://github.com/kentcdodds/babel-plugin-preval) &
[`codegen`](https://github.com/kentcdodds/babel-plugin-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](https://github.com/ceteio/next-layout-loader/issues/1)!)_. To ensure the
layout files are loaded correctly, you must include the `codegen` and `preval`
plugins:
`.babelrc`
```json
{
"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`
```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", [, options])
```
### ``
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:
```javascript
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](https://github.com/ceteio/next-layout-loader/issues/2)_).
### `options`
An object of further options to affect how the library loads layout files.
```javascript
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`:
```javascript
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 }) => (
));
```
`pages/dashboard/user/index.tsx`:
```javascript
export default function User() {
return
Hello world
;
}
```
`next-layout-loader` will transform the `pages/app.tsx` into:
```javascript
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 (
{renderedComponent}
);
};
})();
export default withLayoutLoader(({ 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](https://github.com/vercel/next.js/discussions/26389#discussioncomment-922493)
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`:
```diff
pages
├── _layout.tsx
-├── user.tsx
└── user
+ ├── index.tsx
└── _layout.tsx
```