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 🙏

© 2024 – Pkg Stats / Ryan Hefner

next-page-layout

v1.0.3

Published

A type safe, zero dependency layout solution with data fetching capabilities for Next.js.

Downloads

38

Readme

Next Page Layout

A type safe, zero dependency layout solution with data fetching capabilities for Next.js.

Features

  • Persisted layout state when navigating between pages *
  • Server-side layout data population with the usual suspects (getInitialProps, getServerSideProps and getStaticProps)
  • Client-side layout data population ("useInitialProps")
  • Nested layouts with per-layout data requirements, all fetched in parallel
  • A type safe API using Typescript

* Similar to layouts recently added to Nextjs.

Current status

This library is fairly new and doesn't have a large user base yet. However, I've been running this in production in several applications for quite some time already, and it's working like a charm. I suggest you give it a try and let me know how it works. I'm commited to maintaining this lib.

Background

Pages in Nextjs don't come with a hierarchy (like e.g. https://reactrouter.com/). If you want to render 2 pages with a shared layout, this layout must be rendered individually by both pages. By default this means that the shared layout will remount whenever navigating between the pages (as the top-level page component changes). This has been a recurring topic in the community and was recently addressed by Nextjs when they introduced layouts.

Nextjs layouts solve the remount issue but it's far from a perfect solution:

  • Server-side data fetching (and SSR) is not supported
  • There's no convenient way to duplicate server-side logic on multiple pages (App does not support data fetching methods)
  • While client-side data fetching is possible, there is no built-in mechanism to prevent a waterfall effect when rendering nested layouts with data requirements.

Getting started

  1. Add next-page-layout to your Nextjs project:
npm i next-page-layout
  1. Wrap page rendering in your custom App with <LayoutPageRenderer>:
import { LayoutPageRenderer } from 'next-page-layout';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <LayoutPageRenderer
      page={Component}
      initialProps={pageProps}
      errorComponent={ErrorComponent}
      loadingComponent={LoadingComponent}
    />
  );
}
  1. Define layouts and pages using the exported makeLayout() and makeLayoutPage() functions.
  2. To properly support server-side rendering, instrument page rendering in your custom Document with prepareDocumentContext():
import { prepareDocumentContext } from 'next-page-layout';

export default class CustomDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    await prepareDocumentContext(ctx);
    return Document.getInitialProps(ctx);
  }
}

NOTE: This will cause a React hydration warning in development but it should be safe to ignore this.

The reason this happens is that when SSRing, we need to render the page twice for useParentProps() described below to work. The resulting HTML is passed to the client, which sees a mismatch when composing the initial component tree. This is instantly corrected with useLayoutEffect() which is why it should be safe to ignore this warning.

  1. Clone the code and run npm run example to see the included example Nextjs app in action.

next-page-layout was created specifically to solve these issues.

Basic Usage

Layouts are created with makeLayout(). Consider this example:

import { makeLayout } from 'next-page-layout';

interface LayoutProps {
  title: string;
  children: ReactNode;
}

export const Layout = makeLayout(
  {
    // You can use any of:
    //
    // getInitialProps
    // getServerSideProps
    // getStaticProps
    //
    // But note that you cannot mix&match layouts
    // with different server-side data fetching methods.
    //
    // See example app.
    getInitialProps: async (context) => {
      await sleep(300);
      return {
        title: 'I am a title!',
      };
    },
  },
  {
    // Must be defined even for top-level layouts to make types work.
    useParentProps: () => ({}),
    component: (props: LayoutProps) => {
      return (
        <div>
          <h1>{props.title}</h1>
          <div>{props.children}</div>
        </div>
      );
    },
  }
);

Here we create (and export) a layout rendering a title using data fetched with getInitialProps. When server-side rendering, getInitialProps will run on the server similarly to how it would if this was a regular Nextjs page.

To render this layout as part of a page, we need to export makeLayoutPage() in a regular Nextjs page file:

import { makeLayoutPage } from 'next-page-layout';

export default makeLayoutPage(
  {
    getInitialProps: async (context) => {
      await sleep(300);
      return {
        content: 'I am a page!',
      };
    },
  },
  {
    layout: Layout,
    useLayoutProps: (props) => ({ title: 'Overridden title' }),
    component: (props) => {
      return <>{props.content}</>;
    },
  }
);

The page above defines its own getInitialProps but we don't have to call getInitialProps on the layout explicitly since the library takes care of this. In useLayoutProps we have the option to pass props to our layout (such as an overridden title). This is all type safe thanks to Typescript and type inference 👍

Nested layouts

Nested layouts are supported by passing the parent layout when calling makeLayoutPage():

import { makeLayout } from 'next-page-layout';

interface ChildLayoutProps {
  subtitle: string;
  children: ReactNode;
}

export const ChildLayout = makeLayout(
  {
    getInitialProps: async (context) => {
      await sleep(300);
      return {
        subtitle: 'I am a subtitle!',
      };
    },
  },
  {
    parent: Layout,
    useParentProps: (props) => ({}),
    component: (props: ChildLayoutProps) => {
      return (
        <div>
          <h2>{props.subtitle}</h2>
          <div>{props.children}</div>
        </div>
      );
    },
  }
);

Above we create a child layout to our earlier Layout. We'd create a page with this layout just like we created the page with the parent Layout. getInitialProps, useLayoutProps and type inference still work out of the box 👍

Client-side data fetching and useInitialProps

Sometimes we might want to do some data fetching on the client. Despite the obvious drawbacks, client-side data fetching has the following benefits:

  • Authentication info might only be available on the client, preventing the server from pre-fetching data (e.g. SSO and/or external auth)
  • The default UX when navigating between Nextjs pages can become "unresponsive" - to give a feeling of instant navigation, it might be better to instantly render a loading indicator in the UI where content is being updated ( preserving the layout).

next-page-layout supports client-side data fetching with useInitialProps. Here's an example of Layout and ChildLayout used in our previous example but this time with client-side data fetching. In this example we use SWR to fetch data, but any solution with a similar API works well ( e.g. Apollo GraphQL and useQuery).

import {
  makeLayout,
  makeLayoutPage,
  wrapSwrInitialProps,
} from 'next-page-layout';
import useSWR from 'swr';

// Parent layout.

interface LayoutProps {
  title: string;
  children: ReactNode;
}

export const Layout = makeLayout(
  {
    useInitialProps: () => {
      return wrapSwrInitialProps(
        useSWR('parent', async () => {
          await sleep(300);
          return { title: 'I am a title!' };
        })
      );
    },
  },
  {
    useParentProps: (props) => ({}),
    component: (props: LayoutProps) => {
      return (
        <div>
          <h1>{props.title}</h1>
          <div>{props.children}</div>
        </div>
      );
    },
  }
);

// Child layout.

interface ChildLayoutProps {
  subtitle: string;
  children: ReactNode;
}

export const ChildLayout = makeLayout(
  {
    useInitialProps: () => {
      return wrapSwrInitialProps(
        useSWR('child', async () => {
          await sleep(300);
          return { subtitle: 'I am a subtitle!' };
        })
      );
    },
  },
  {
    parent: Layout,
    useParentProps: (props) => ({}),
    component: (props: ChildLayoutProps) => {
      return (
        <div>
          <h2>{props.subtitle}</h2>
          <div>{props.children}</div>
        </div>
      );
    },
  }
);

// Page.

export default makeLayoutPage(
  {
    useInitialProps: () => {
      return wrapSwrInitialProps(
        useSWR('page', async () => {
          await sleep(300);
          return { content: 'Page' };
        })
      );
    },
  },
  {
    layout: ChildLayout,
    useLayoutProps: (props) => ({
      subtitle: 'Overriden subtitle!',
    }),
    component: (props) => {
      return <>{props.content}</>;
    },
  }
);

Note that while the example above renders 3 levels of components (Layout, ChildLayout and Page), all using client-side data fetching, there's no waterfall effect. All data fetching happens in parallel! Also note that there's nothing stopping you from mixing and matching layouts/pages with both getInitialProps and useInitialProps. 👍

useParentProps

The signature for useParentProps is slightly awkward.

type UseParentProps<
  TProps extends LayoutBaseProps,
  TInitialProps extends Partial<TProps>,
  TParent extends Layout<any, any, any> | undefined
> = (props: {
  initialProps: InitialProps<TInitialProps>;
  layoutProps: InitialProps<LayoutSelfProps<TProps, TInitialProps>>;
  requireInitialProps: (
    callback: (initialProps: TInitialProps) => LayoutProps<TParent>
  ) => any;
  requireLayoutProps: (
    callback: (
      layoutProps: LayoutSelfProps<TProps, TInitialProps>
    ) => LayoutProps<TParent>
  ) => any;
  requireProps: (
    callback: (props: {
      initialProps: TInitialProps;
      layoutProps: LayoutSelfProps<TProps, TInitialProps>;
    }) => LayoutProps<TParent>
  ) => any;
}) => LayoutProps<TParent>;

useParentProps gets passed an object with various properties and is expected to return "layout props" for the parent. The various properties on the object are:

initialProps

These are the initial props fetched with either getInitialProps or useInitialProps, wrapped in InitialProps which is a data loader wrapper containing the state of the loaded data.

export type InitialProps<TInitialProps> = {
  data: TInitialProps | undefined;
  loading?: boolean;
  error?: Error;
};

layoutProps

These are the "layout props" passed to the layout by a child layout (or page). These are also wrapped in the same data loader wrapper.

requireInitialProps, requireLayoutProps and requireProps

If we'd only support server-side data fetching with getInitialProps, the signature would be a lot simpler. In this case we wouldn't need the data loader wrapper InitialProps at all and could simply have useParentProps accept the resolved initialProps and layoutProps.

However, since we support client-side data fetching, we need to account for the fact that both initialProps and layoutProps might not be resolved when the layouts are rendered and useParentProps called. If you have a layout which passes some property to a parent layout, and this property is based on data loaded on the client, you have 2 options:

  • You pass a placeholder value if data is still being loaded OR
  • You use any of the require functions to signal that the loading state of the parent layout is dependent on the child layout

Here's an example of the latter, using the page in our previous example, but passing data loaded client-side to the parent:

import { makeLayoutPage, wrapSwrInitialProps } from 'next-page-layout';
import useSWR from 'swr';

export default makeLayoutPage(
  {
    useInitialProps: () => {
      return wrapSwrInitialProps(
        useSWR('page', async () => {
          await sleep(300);
          return { content: 'Page' };
        })
      );
    },
  },
  {
    layout: ChildLayout,
    useLayoutProps: (props) =>
      props.requireInitialProps((initialProps) => ({
        subtitle: `Overridden ${initialProps.content}`,
      })),
    component: (props) => {
      return <>{props.content}</>;
    },
  }
);

Support

Feel free to open an issue or reach out to @abergenw.