🏄
Matsuri-tech Frontend Weekly 2023-04-14

hrdtbs

hrdtbs

2023年4月14日
MFW

社内向けに、フロントエンド関連のニュースや業務で発生したQ&A、利用しているライブラリなどの情報を定期的に書いています。

この記事は、社内の開発部メンバーに向けて直近でリリースされたライブラリなどの情報を不定期でまとめたものです。内容は実務や趣味で使えそうなものを中心に扱っており、網羅的ではなく偏りがあります。

# Q & A

普段の業務において、質問を受けた際の回答などで有益そうなものをピックアップしています。

# Q. 社用eslint-configを導入したら、hasSuggestionを設定しろと言われた

状況

社用eslint-configは、eslintとそれのみを導入すれば良いように構成されており、ESLint v8に依存していたため、社用eslint-configの導入と同時にESLint v8へのアップグレードも行われていました。

出力されていたエラーは、ESLint v8から変更を提案するルールにおいてhasSuggestionが必須になったため発生したものです。エラーを見ると、react-hooks/exhaustive-depsで発生していました。

ですが、社用eslint-configが依存しているeslint-plugin-react-hooksバージョンは最新であり、hasSuggestionが設定されていました。また社用eslint-configでは、CIで自己テストをしており、エラーは発生していませんでした。

原因

これは社用eslint-configを導入したリポジトリに、古いバージョンのhasSuggestionが指定されていないeslint-plugin-react-hooksのバージョンが別途指定されていたために発生していました。

このようにパッケージを導入したり更新をしてエラーが出た場合、パッケージの依存関係を確認すると、すぐに原因を特定できるケースがあります。次のコマンドが便利です。

npm ls -a

このコマンドは、パッケージの依存関係に基づいた論理的依存ツリーを出力します。次のような出力が行われます。実際はもっと複雑なツリーが表示されると思います。

my-app@1.0.1
├── eslint@8.38.0
├── ...
├─┬ eslint-config-hoge@3.0.0
│ ├── ...
│ └── eslint-plugin-react-hooks@4.6.0
└── eslint-plugin-react-hooks@4.2.0

コマンドの引数に確認したいパッケージ名を指定すれば、絞り込んで表示できます。

npm ls -a eslint-plugin-react-hooks

my-app@1.0.1
├─┬ eslint-config-hoge@3.0.0
│ └── eslint-plugin-react-hooks@4.6.0
└── eslint-plugin-react-hooks@4.2.0

# Chrome 112

2023年4月4日にChrome 112がリリースされました。影響のある新機能はほぼなかったので、全く話題に上がっていません。

# ネスティングCSS

Chrome 112では、CSSのネストがサポートされました。

次のような記述が出来ます。

.nesting {
  color: hotpink;

  > .is {
    color: rebeccapurple;

    > .awesome {
      color: deeppink;
    }
  }
}

これは次の記述と同等です。

.nesting {
  color: hotpink;
}

.nesting > .is {
  color: rebeccapurple;
}

.nesting > .is > .awesome {
  color: deeppink;
}

CSSのネストは、Edge 112でもサポートされ、Safari 16.5のTechnology Previewにも含まれているため、ChromeやSafariのみサポートすれば良い環境では近い内に実務でも利用して良くなると思います。一方、それ以外の環境ではFirefoxをはじめ軒並みサポートされていないため、雑に利用できるようになるには、まだしばらくかかると思われます。

また、いくつかのフォーマッタはCSSのネストをまだサポート出来ていないため、利用には注意が必要です。

# Storybook v7

2023年4月12日にStorybook v7が正式リリースされました。Storybook v7では様々な最適化や機能の統廃合が行われ、今までより全体的に扱いやすくなっています。Monorepoであったり特殊な構成をしていない限り、マイグレーションガイドに従えば、すぐに移行は完了します。

# UIデザインの刷新

UIが全体的に刷新され、より重要な情報にアクセスしやすくなりました。

特にドキュメントがタブで切り替えて表示する形式から、コンポーネントの表示される箇所でそのまま表示されるようになったため、以前よりドキュメントへのアクセスが簡単になりました。

https://storybookblog.ghost.io/content/images/size/w1600/2023/04/Tom-SB7-Docs.005.png

# First-class Framework integrations

Storybook v7ではいくつかのフレームワークを決め打ちして、ゼロコンフィグで利用できるような対応が行われています。

現在はVite、Next.js、SvelteKitで導入されており、今後RemixやNuxtも予定されています。

Next.jsの場合、Webpack、Babel、Turbopackなどのビルド設定のミラーリングによるゼロコンフィグだけでなく、next/imageやnext/routerなどのモックが挿入されるため、以前よりはるかに導入しやすくなっています。

# Interaction testing - Group steps

Storybookのインタラクションテストでは、今までjestのdescribeやitのようにそのテストコードが何をするためのものなのか表す手段やテストをグループ化する存在しませんでした。

今後は、次のようにstepメソッド利用して、ヒューマンリーダブルなグループ化を行えます。

// SignupForm.stories.ts
import type { Meta, StoryObj } from "@storybook/your-framework";
import { userEvent, within } from "@storybook/testing-library";
import { SignupForm } from "./SignupForm";
const meta: Meta<typeof SignupForm> = {
  title: "SignupForm",
  component: SignupForm,
};
export default meta;
type Story = StoryObj<typeof SignupForm>;

export const Submitted: Story = {
  play: async ({ args, canvasElement, step }) => {
    const canvas = within(canvasElement);
    await step("Enter email and password", async () => {
      await userEvent.type(canvas.getByTestId("email"), "hi@example.com");
      await userEvent.type(canvas.getByTestId("password"), "supersecret");
    });
    await step("Submit form", async () => {
      await userEvent.click(canvas.getByRole("button"));
    });
  },
};

# Next.js v13.3

2023年4月7日にNext.js v13.3がリリースされました。

# ファイルベースMetadata API

Next.js v13.2で追加されたコンフィグベースのMetadata APIに加えて、ファイル規則によってメタデータをカスタマイズ出来るようになりました。この機能は、App Routerでのみ利用できます。

次のように利用します。

app
├── favicon.(ico|jpg|png|svg)
├── sitemap.(xml|js|jsx)
├── robots.(txt|js|jsx)
├── manifest.(json|js|jsx)
├── layout.js
├── page.js
└── about
    ├── opengraph-image.(jpg|png|svg)
    ├── twitter-image.(jpg|png|svg)
    └── page.js

配置されたfavicon、sitemap、 robots、manifest、opengraph-image、twitter-imageは自動でheadに反映されます。

動的なファイル生成

JSやJSXをサポートしているsitemapやrobots、manifestでは動的な生成が可能です。

export default async function sitemap() {
  const res = await fetch('https://.../posts');
  const allPosts = await res.json();

  const posts = allPosts.map((post) => ({
    url: `https://acme.com/blog/${post.slug}`,
    lastModified: post.publishedAt,
  }));

  const routes = ['', '/about', '/blog'].map((route) => ({
    url: `https://acme.com${route}`,
    lastModified: new Date().toISOString(),
  }));

  return [...routes, ...posts];
}

# 動的なOGP及びTwitter画像生成

ファイルベースMetadata APIにおけるopengraph-imageやtwitter-imageも動的な生成が可能です。拡張をjsやjsxにして次のようにします。

// /app/about/opengraph-image.tsx
import { ImageResponse } from "next/server";

export const size = { width: 1200, height: 600 };
export const alt = "About Acme";
export const contentType = "image/png";

export default function og() {
  // 中身であるsatoriと同様の記述が出来ます。
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: "white",
          width: "100%",
          height: "100%",
          display: "flex",
          textAlign: "center",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        Hello world!
      </div>
    ),
    {
      width: 1200,
      height: 600,
    }
  );
}

# App Routerの静的エクスポート

App Routerを完全に静的なファイルとして出力できるようになりました。

**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  output: 'export',
};

module.exports = nextConfig;

静的エクスポートの場合、サーバーコンポーネントや動的な画像生成などは、ビルド中に実行されます。ただし、サーバーが必要なheaders()やcookies()などは実行出来ません。

また、この変更によりnext exportコマンドは不要になりました。

# Interception Routes

Interception routesはURLを変化させつつ、現在のレイアウトに新しいページを表示します。Interception routesでは、相対パスの../を表す(..)やappからの相対パスを表す(…)を利用します。

次はVercelが用意したサンプルアプリケーションの構成です。

app
├── @modal
│   └── (..)photos
│       └── [id]
│           └── page.tsx
├── page.tsx
├── layout.tsx
└── photos
		└── [id]
		    └── page.tsx

ルートページには画像一覧があり、画像をクリックすると/app/@modal/(..)photos/[id]/page.tsxで定義された画像を子要素に持つモーダルが表示されますが、このときURLは../photos/[id]になります。この状態でページをリロードすると、表示されるのは/app/photos/[id]/page.tsxの内容になります。

# Parallel Routes

Parallel routesは、同じレイアウトの中に複数のページを表示します。Interception Routesで紹介したサンプルアプリケーションでは、この機能によってルートページに画像一覧とモーダルを同時に表示しています。

Parallel routesは、Web componentsのslotをイメージすると良いと思います。ディレクトリ名の先頭@を付けて、名前付きのスロットとして定義して利用します。サンプルアプリケーションでは@modalがこれに相当します。

app
├── @modal
│   └── (..)photos
│       └── [id]
│           └── page.tsx
├── page.tsx
├── layout.tsx
└── photos
		└── [id]
		    └── page.tsx

名前付きのスロットは同じ階層にあるlayout.jsのpropsから利用します。サンプルアプリケーションではapp/layout.jsが相当します。

export default function Layout(props) {
  return (
    <html>
      <body>
        <GithubCorner />
        {props.children}
        {props.modal}
      </body>
    </html>
  );
}