WENPE PLAYGROUND
WENPE PLAYGROUND
WENPE PLAYGROUND
WENPE PLAYGROUND

Image Not Found
#Next.js

2023.02.25

Next.jsにLayoutsを適用する

概要

  • Next.jsでブログを作成しているのですが、共通のコンポーネントをページごとに定義しており、メンテナンス性がよくないという課題がありました。
  • そこで、Next.jsの機能であるLayoutsを利用して、ページごとの共通のレイアウトを作成したいと思います。

ゴール

  • 共通のコンポーネントをレイアウトとして外だしする

作成物

参考

手順

レイアウトの作成

  • まずは、 Headerコンポーネントと Headerコンポーネントで構成されるLaytoutコンポーネントを作成します。
// components/Header.tsx
import { AppBar, Box, Typography, Container, Divider } from '@mui/material';
import { NextMuiLink } from 'components/NextMuiLink';

export const Header = () => {
  return (
    <div>
      <AppBar position='static' sx={{ boxShadow: `none`, backgroundColor: `#062127` }}>
        <Container maxWidth='xl'>
          <Box
            sx={{
              display: `flex`,
              alignItems: `center`,
              height: `70px`,
            }}
          >
            <NextMuiLink href='/'>
              <Typography sx={{ fontSize: `25px` }}>Wenpe Playground</Typography>
            </NextMuiLink>
          </Box>
        </Container>
      </AppBar>
      <Divider light sx={{ borderColor: `rgba(211, 211, 211, .3)` }} />
    </div>
  );
};


// components/Layouts.tsx
import { ReactElement } from 'react';
import { Header } from 'components/Header';


type LayoutProps = Required<{
  readonly children: ReactElement;
}>;


export const Layout = ({ children }: LayoutProps) => (
  <>
    <Header />
    {children}
  </>
);



レイアウトの作成

  • 次に、NextPageとAppPropsを参考ページにあるように、拡張します。
// types/layout.ts
import type { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';

export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode;
};

export type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout;
};


_app.tsxの修正

  • _app.tsxを修正し、ページごとにレイアウトを変更できるようにします
  • Component.getLayoutがない場合はそのままpageを返し、ある場合はページごとにレイアウトを定義します
  • 私の場合は別のプラグインも入っているので、以下のようになりました。
// pages/_app.tsx
import type { AppPropsWithLayout } from 'types/layout';
import PropTypes from 'prop-types';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { CacheProvider, EmotionCache } from '@emotion/react';
import theme from 'styles/theme';
import createEmotionCache from 'styles/createEmotionCache';
import nextSeoConfig from 'nextSeoConfig';
import { DefaultSeo } from 'next-seo';

// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();

interface MyAppProps extends AppPropsWithLayout {
  emotionCache?: EmotionCache;
}

function MyApp(props: MyAppProps) {
  const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
  const getLayout = Component.getLayout ?? ((page) => page);

  return getLayout(
    <CacheProvider value={emotionCache}>
      <DefaultSeo {...nextSeoConfig} />
      <ThemeProvider theme={theme}>
        {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
        <CssBaseline />
        <Component {...pageProps} />
      </ThemeProvider>
    </CacheProvider>,
  );
}

MyApp.propTypes = {
  Component: PropTypes.elementType.isRequired,
  emotionCache: PropTypes.object,
  pageProps: PropTypes.object.isRequired,
};

export default MyApp;


レイアウトをページごとに定義して呼び出す

  • getLayoutを呼び出し、ページごとのレイアウトを定義します
import type { ReactElement } from 'react';
import type { InferGetStaticPropsType } from 'next';
import type { NextPageWithLayout } from 'types/layout';
import { client } from 'libs/client';
import type { Blog, Tag } from 'types/blog';
import { Container, Grid } from '@mui/material';
import { BlogCard } from 'components/BlogCard';
import { NextMuiLink } from 'components/NextMuiLink';
import { Layout } from 'components/Laytout';

// Get blog and tags data
export const getStaticProps = async () => {
  const blog = await client.get({ endpoint: 'blog' });
  const tag = await client.get({ endpoint: 'tag' });

  return {
    props: {
      blogs: blog.contents,
      tags: tag.contents,
    },
  };
};

type Props = {
  blogs: Blog[];
  tags: Tag[];
};

const Home: NextPageWithLayout<InferGetStaticPropsType<typeof getStaticProps>> = ({
  blogs,
}: Props) => {
  return (
    <div>
      <Container maxWidth='xl' sx={{ marginTop: `20px` }}>
        <Grid container rowSpacing={2} columnSpacing={2}>
          {blogs.map((blog) => (
            <Grid key={blog.id} item xs={12} md={6} lg={4}>
              <NextMuiLink href={`/blog/${blog.id}`}>
                <BlogCard
                  imagePath={blog.image.image.url}
                  imageAlt='Image Not Found'
                  title={blog.title}
                  tags={blog.tags}
                ></BlogCard>
              </NextMuiLink>
            </Grid>
          ))}
        </Grid>
      </Container>
    </div>
  );
};

Home.getLayout = function getLayout(page: ReactElement) {
  return <Layout>{page}</Layout>;
};

export default Home;


  • 上記で完了です。

© 2023 Wenpe