クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【React】Nextスターターキットを作った-1. 全体設定編(Next + TypeScript + CSS in JS)

Nextの続きです。前回はCreate Next App + TypeScript + CSS in JS環境を作りました。今回は以前作ったReactスターターキットをNext.jsで作り直し、挫折していたOGP問題を解決しました!今回は全体にわたる環境設定編です。それではいきましょう!

【目次】

※参考:前回記事
【React】Create Next App + TypeScript + CSS in JS環境を作る - クモのようにコツコツと

※参考:ReactでWebアプリを作るシリーズまとめ
qiita.com

前回のおさらい

Create Next App + TypeScript + CSS in JS環境を作った https://cdn-ak.f.st-hatena.com/images/fotolife/i/idr_zz/20210424/20210424183903.jpg

別ページにも固有のtitle、descriptionを設定できた! https://cdn-ak.f.st-hatena.com/images/fotolife/i/idr_zz/20210424/20210424183953.jpg

※参考:前回記事
【React】Create Next App + TypeScript + CSS in JS環境を作る - クモのようにコツコツと

これなら以前、Reactスターターキットで挫折した固有のOGP設定もできるのでは!?

※参考:【React】OGPはつらいよ ーSPAでの動的OGP・失敗編ー(Reactアプリスターターキット) - クモのようにコツコツと

作ったもの

ということで作ってみた。

ソースコード

※参考:GitHub - ryo-i/next-app-started

プレビュー

※参考:Nextアプリスターターキット


トップページ
f:id:idr_zz:20210501124640j:plain

はてなブログでもOGPが表示されとる! next-app-started.vercel.app


Aboutページ
f:id:idr_zz:20210501124656j:plain

※参考:このアプリについて | Nextアプリスターターキット

Aboutページも固有のOGPが表示される! next-app-started.vercel.app

これまでのスターターキット

Nextスターターキットのベースとなる過去のスターターキットたち。見た目はほとんど変わらない。

Webコーディング スターターキット

ソース

※参考:GitHub - ryo-i/web-coding-getting-sterted: HTML/CSS/JSコーディングの最小環境です。

プレビュー

※参考:Webコーディング スターターキット

静的なHTML、CSS、JSのみで書いている。懸念点はページを増やすたびに重複コードのあるHTMLページを増やさなければならない。共通部分の修正はHTMLページ分になる。

※参考:【メタ言語】フロントエンド開発スターターキットを作った(EJS、Sass(SCSS)、TypeScript) - クモのようにコツコツと

フロントエンド開発スターターキット

ソース

※参考:GitHub - ryo-i/front-end-getting-sterted: メタ言語(EJS、Sass(SCSS)、TypeScript)のコンパイル環境です。画像も圧縮します。

プレビュー

※参考:フロントエンド開発スターターキット

EJS + Sass(SCSS) + TypeScript環境で共通部分はモジュール化している。EJSとSassはgulpで、TypeScriptはwebpackでコンパイルする。懸念点はEJSとSassがほとんど同じ構成だが別ファイルのため構成変更時は修正が2箇所になる。

※参考:【メタ言語】フロントエンド開発スターターキットを作った(EJS、Sass(SCSS)、TypeScript) - クモのようにコツコツと

Reactスターターキット

ソース

※参考:GitHub - ryo-i/react-app-started

プレビュー

※参考:Reactアプリスターターキット

React + TypeScript + CSS in JS環境。共通部分はコンポーネントとして1ファイルに書いているため、修正は一箇所で済む。懸念点はSPAのためページ固有のOGPが設定できなかった。

※参考:【React】OGPはつらいよ ーSPAでの動的OGP・失敗編ー(Reactアプリスターターキット) - クモのようにコツコツと

このOGP問題を解決するために今回Next.js環境でスターターキットを作り直した!

環境構築

まずCreat Next App + TypeScript + CSS in JS環境を作る。前回と同じ手順。

※参考:【React】Create Next App + TypeScript + CSS in JS環境を作る - クモのようにコツコツと


Creat Next Appをインストールした直後のコミット

※参考:GitHub - ryo-i/next-app-started at 5dfd72546ec60222518947e79828156e16dfe9f2

TypeScriptとCSS in JS(Styled-components)を追加したコミット

※参考:GitHub - ryo-i/next-app-started at 68b4206cb51944f398066b7f80a9968670ec4ee6

CSSホットリロード設定(Babel)

ローカルで開発モード(npm run dev)で修正しているとCSSの変更が反映されなかった。修正のたびにStyled-componentsから別のclass名が振られるためだった。

※参考:Next.jsでclassNameが見つからなくなるバグの対処方 - Qiita

.babelrcファイルを追加してBabelに下記の設定を追加したら反映されるようになった!

{
    "presets": ["next/babel"],
    "plugins": [
      [
        "styled-components",
        { "ssr": true, "displayName": true, "preprocess": false }
      ]
    ]
  }

※参考:Warning: Prop `className` did not match. · Issue #7423 · vercel/next.js · GitHub

OGP設定

静的ファイルを入れる「/public」フォルダを作成しOGP設定に必要な画像ファイル類と「manifest.json」を追加
f:id:idr_zz:20210501173044j:plain

画像は前回のSPAのOGP設定のときと共通

※参考:【React】Create Next App + TypeScript + CSS in JS環境を作る - クモのようにコツコツと


manifest.jsonの設定

{
  "short_name": "next-started",
  "name": "next-app-started",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    },
    {
      "src": "android-chrome-192x192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "android-chrome-512x512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

short_namenameの値を打ち替えている。


「/data」フォルダを作成し「data.json」を追加

{
    "head": {
        "url": "https://next-app-started.vercel.app/"
    },
   // 後略
  • headキーにデプロイ先のURLを設定

「/components」フォルダを作成し「CommonHead.tsx」を追加

data.jsonをインポートし、変数urlでデプロイ先のURLを取得

import Data from '../data/data.json';

const url = Data.head.url;

CommonHeadコンポーネントでOGPの共通部分を設定(URLにはurlを設定)

// Component
function CommonHead() {
  return (
    <>
      <meta property="og:url" content={url} />
      <meta property="og:image" content={url + "ogp.png"} />
      <meta property="og:type" content="website" />
      <meta name="twitter:site" content="@idr_zz" />
      <meta name="twitter:image" content={url + "ogp.png"} />
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <meta name="theme-color" content="#000000" />
      <link rel="manifest" href="/manifest.json" />
      <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
      <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
      <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
      <link rel="manifest" href="/site.webmanifest" />
      <meta name="msapplication-TileColor" content="#da532c" />
      <meta name="theme-color" content="#ffffff" />
    </>
  );
}

CommonHeadコンポーネントをエクスポート

export default CommonHead;

「/pages」フォルダの中に「_app.tsx」を追加。初期設定をカスタマイズできる。

※参考:Advanced Features: カスタム`App` | Next.js

HeadコンポーネントとCommonHeadコンポーネントをインポート

import Head from 'next/head'
import CommonHead from '../components/CommonHead';

HeadCommonHeadを読み込む

function MyApp({ Component, pageProps }) {
  // 中略

  return (
    <>
      <Head>
        <CommonHead />
      </Head>
      // 中略
      <Component {...pageProps} />
    </>
  )
}

export default MyApp

Component {...pageProps}の部分

pagePropsはデータ取得メソッドの 1 つによってプリロードされた初期 props を持つオブジェクトです。そうでなければ空のオブジェクトになります。

※参考:Advanced Features: カスタム`App` | Next.js

共通CSS設定

「/styles」フォルダを作成し「variables.json」を追加

{
    "variable": {
        "baseColor": "#A63744",
        "textColor": "#333",
        "textColor_w": "#fff",
        "bgColor_g": "#eee",
        "textSize": "14px"
    }
}

使い回すCSSの値を設定する


「/styles」フォルダに「mixin.ts」を追加

import styled, { css } from 'styled-components';

const pageSize = css`
    width: 100%;
    max-width: 1000px;
    margin: 0 auto;
`;

export {pageSize};

ページサイズの共通CSS、pageSizeコンポーネントを設定


「_app.tsx」でcreateGlobalStylepageSizecssVariablesをインポート

import { createGlobalStyle } from 'styled-components';
import { pageSize } from '../styles/mixin';
import cssVariables from '../styles/variables.json';

変数variablecssVariablesのCSS変数を取得

const variable = cssVariables.variable;

変数GlobalStyleでグローバルCSSを設定(variablepageSizeを読み込む)

// Style
const GlobalStyle = createGlobalStyle`
  body {
    margin: 0;
    padding: 0;
    font-family: sans-serif;
    font-size: ${variable.textSize};
    color: ${variable.textColor};
    *, *:before, *:after {
        box-sizing: border-box;
    }
    a {
        color: ${variable.baseColor};
        &:hover {
          opacity: 0.8;
        }
    }
    section {
      margin: 30px 0;
    }
    h1, h2, p, figure, ul, table {
      margin: 0 0 10px;
    }
    h1, h2 {
      line-height: 1.25;
    }
    p {
      line-height: 1.75;
      margin: 0 0 10px;
    }
  }
  main {
    ${pageSize}
    padding: 50px 50px 0;
    @media(max-width: 600px) {
      padding: 30px 30px 0;
    }
    h1 {
      font-size: 1.5em;
    }
    h2 {
      font-size: 1.25em;
      color: ${variable.baseColor};
    }
  }
`;

styled-componentsでのmedia-query設定は入れ子でOKだった。

※参考:styled-components: FAQs


MyAppGlobalStyleを読み込む

function MyApp({ Component, pageProps }) {
  // 中略

  return (
    <>
      // 中略
      <GlobalStyle />
      <Component {...pageProps} />
    </>
  )
}

FOUC対策

ローカル環境では気がつかなかったのだが、クラウド環境(Vercel)にデプロイしたらCSSが一瞬まったく当たらない状態がチラついて表示される。これは「FOUC(Flash of unstyled content)」というらしい。

※参考:CSSのロードタイミングの不備が引き起こす、FOUCとは? - ふろしき.js


ローカル環境ではheadタグの中に<style data-next-hide-fouc="true">body{display:none}</style>というタグが追加されるためFOUCにならない。しかしクラウド環境ではこのタグは欠落してしまう。 f:id:idr_zz:20210501183720j:plain

※参考:next.js 🚀 - 開発サーバーにスタイルなしコンテンツのフラッシュがあります(FOUC) | bleepcoder.com


こちらの事例は「CSS Modules」だが「_document.js」に空のscriptタグ<script></script>タグを追加したら解決したと(自分の環境ではうまくいかなかった)

※参考:Next.js + CSS ModulesでFOUC(CSSの適用遅れによるちらつき)が発生したときの暫定対策

「_document.js」はNext.jsのhtmlタグ、bodyタグをカスタマイズできるファイル。

※参考:Advanced Features: カスタム `Document` | Next.js


自分の場合はこちらの方法のほうがうまくいった。

※参考:next.js 🚀 - 追い風+ cssモジュールを使用した開発でちらつくスタイル | bleepcoder.com
※参考:Development server has a flash of unstyled content (FOUC) · Issue #13058 · vercel/next.js · GitHub

しかし、componentDidMount()はクラスコンポーネントでのライフサイクルで、自分の場合は関数コンポーネント。

フックの場合はtypeof window !== "undefined"というif文の判定を使う方法もあるようだ。

※参考:Next.jsで"document is not defined." "window is not defined."のエラーが出たときの対処法 - Qiita

また、フックでcomponentDidMount()に代わるライフサイクルはuseEffect()もある。

※参考:【React】フックとjson-serverをFetch APIで連携(Reactライフサイクルの理解) - クモのようにコツコツと


「/styles」フォルダに「styles.css」を追加

.no-fouc {
    visibility: hidden;
    opacity: 0;
}

.fouc {
    visibility: visible;
    opacity: 1;
}

.no-foucでは非表示、.foucでは表示、という設定

この設定、当初は「_app.tsx」のGlobalStyleの中に書いていたがそれだとFOUCのままだった。

前回のCreate Next AppではFOUCが起こらないのだが、こちらCSSは「/styles」フォルダの中にcssファイルとして設定されていたので、同じ方法をとってみた。

※参考:ようこそ、ねくすと・じぇいえすへ


「/pages」フォルダに「_document.tsx」を追加

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html className="no-fouc">
        <Head />
        <body>
            <Main />
            <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

基本的にNextリファレンスに従った内容だが一点、Htmlに先ほどの非表示スタイル.no-foucを追加している。

※参考:Advanced Features: カスタム `Document` | Next.js


「_app.tsx」でstyles.cssを読み込む

import '../styles/styles.css';

MyAppで副作用フックuseEffect()を設定

function MyApp({ Component, pageProps }) {
  // For FOUC
  useEffect(() => {
    const removeFouc = (foucElement) => {
      foucElement.className = foucElement.className.replace('no-fouc', 'fouc');
    };
    removeFouc(document.documentElement);
  });

  // 中略
}
  • 変数removeFoucはアロー関数で引数はfoucElement
    foucElementのclass名の.no-fouc.foucに置換
  • removeFouc()を実行、引数はdocument.documentElement

中身は先ほどのFOUC対策の処理

※参考:next.js 🚀 - 追い風+ cssモジュールを使用した開発でちらつくスタイル | bleepcoder.com

documentElementはルートタグすなわちhtmlタグのこと

※参考:Document.documentElement - Web API | MDN

これでDOMを読み終わったあとに.no-fouc.foucに置換されてページが表示される

最後に

これで全体的な設定は完成。今回得た知見

  • ローカル環境でホットリロードが効かないためBabelの設定を変更する
  • Next.jsの全体にわたる設定は「_app.tsx」で設定
  • htmlタグ、bodyタグは「_document.tsx」で設定
  • クラウド環境のFOUC対策としてhtmlタグを初期状態を非表示にし、DOM読み込み後に表示させる

特にFOUC対策は一番苦戦したので、解決できて良かったです♪

長くなったので今回はここまでにします。次は各ページの内容に入っていきます。

それではまた!


続き書きました!コンポーネント&モジュール編です♪

※参考:www.i-ryo.com


※参考:ReactでWebアプリを作るシリーズまとめ
qiita.com