2040 文字
10 分

Frontend Weekly 2023-04-14

Q & A#

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

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

状況

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

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

https://eslint.org/docs/latest/use/migrate-to-8.0.0#-rules-require-metahassuggestions-to-provide-suggestions

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

原因

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

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

Terminal window
npm ls -a

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

├── ...
│ ├── ...
│ └── [email protected]

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

Terminal window
npm ls -a eslint-plugin-react-hooks
└── [email protected]

Chrome 112#

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

https://developer.chrome.com/blog/new-in-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 であったり特殊な構成をしていない限り、マイグレーションガイドに従えば、すぐに移行は完了します。

https://storybook.js.org/blog/storybook-7-0/

UI デザインの刷新#

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

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

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

First-class Framework integrations#

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

https://storybook.js.org/blog/framework-api/

現在は 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"), "[email protected]");
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 がリリースされました。

https://nextjs.org/blog/next-13-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>
);
}
Frontend Weekly 2023-04-14
https://blog.ohirunewani.com/series/frontend-weekly/2023-04-14/
作者
hrdtbs
公開日
2023-04-14
ライセンス
CC BY-NC-SA 4.0