Yarn v4を使って Next.js 14 + storybook v7 + styled-components のボイラープレートを作る
この記事の趣旨
この記事はコネヒトアドベントカレンダー2023の3日目のコンテンツです。
Next.jsの個人開発をする時、これまでは Tailwind CSS をよく使っていたのですが、社内のフロントエンドではstorybook + styled-componentsでコンポーネントを構築しているケースが多かったため、10月末にリリースされたYarn v4の素振りも兼ねてボイラープレートを作ろうと思ったのがきっかけです。
セットアップ: Next.js のリポジトリを用意する
今回は TypeScript を使用し、app
ディレクトリで開発を行いたかったため以下のように Next.js のセットアップを行いました。
❯ npx create-next-app@latest
✔ What is your project named? … nextjs-styledcomponent-sb
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … No
✔ Would you like to use `src/` directory? … No
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … No
❯ cd nextjs-styledcomponent-sb
Yarn v4 を使うため、ディレクトリで使う Node.js を18系にセットします。
❯ nodenv local 18.18.2
// このコマンドで 4系をリポジトリに導入
❯ yarn set version stable
➤ YN0000: Downloading https://repo.yarnpkg.com/4.0.2/packages/yarnpkg-cli/bin/yarn.js
➤ YN0000: Saving the new release in .yarn/releases/yarn-4.0.2.cjs
➤ YN0000: Done in 0s 86ms
Next.js のセットアップ
styled-components を使うために、next.config.js
を以下のように書き換えます。
/** @type {import('next').NextConfig} */
// 以下のオプションを設定
const nextConfig = {
reactStrictMode: true,
compiler: {
styledComponents: true,
},
}
module.exports = nextConfig
styled-components と関連パッケージのインストール
❯ yarn add styled-components
❯ yarn add -D @types/styled-components babel-plugin-styled-components styled-reset
ここまで行い、yarn dev
で Next.js のスタートページが立ち上がることを確認しましょう。
Next.js と styled-components を統合する
registry.tsx
app
ディレクトリ内にregistry.tsx
というファイルを作成し、styled-components を Next.js の各ファイルで使用できるよう設定します。
参考:https://dev.to/rashidshamloo/using-styled-components-with-nextjs-v13-typescript-2l6m#4
'use client'
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
export default function StyledComponentsRegistry({
children,
}: {
children: React.ReactNode
}) {
// Only create stylesheet once with lazy initial state
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement()
styledComponentsStyleSheet.instance.clearTag()
return <>{styles}</>
})
if (typeof window !== 'undefined') return <>{children}</>
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
)
}
GlobalStyles.ts
次に、globals.css
の中身をまるっと styled-components に置き換えます。styles
ディレクトリを作成し、GlobalStyles.ts
という名前でファイルを作成して下記のように css を移植してください。
参考:https://dev.to/rashidshamloo/using-styled-components-with-nextjs-v13-typescript-2l6m#5
'use client';
import { createGlobalStyle } from 'styled-components';
const GlobalStyles = createGlobalStyle`
// your global styles
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
`;
export default GlobalStyles;
layout への反映
上記のファイルをlayout.tsx
に import し、appディレクトリ内の.tsx
ファイルで styled-components が有効になるよう変更を加えます。
+ 'use client';
- import type { Metadata } from 'next'
- import './globals.css'
+ import GlobalStyles from '../styles/GlobalStyles';
+ import StyledComponentsRegistry from './registry'
- export const metadata: Metadata = {
- title: 'Create Next App',
- description: 'Generated by create next app',
- }
export default function RootLayout(
props: React.PropsWithChildren<{}>
) {
return (
<html lang="en">
<body>
<StyledComponentsRegistry>
<GlobalStyles />
{props.children}
</StyledComponentsRegistry>
</body>
</html>
)
}
ここまで作業が終わったら、yarn dev
で初期ページが崩れずに表示されていることを確認してください。
セットアップ:storybook
以下のコマンドで storybook をセットアップします。yarn dlx
はnpx
ライクなコマンドで、パッケージをダウンロードした後、直接実行できるコマンドです。
yarn dlx storybook init
このままstorybookが起動すれば成功なのですが、私の環境では以下のようなエラーが吐き出されました。
SB_CORE-SERVER_0002 (CriticalPresetLoadError): Storybook failed to load the following preset: ./.yarn/__virtual__/@storybook-nextjs-virtual-8a558cf2c1/3/.yarn/berry/cache/@storybook-nextjs-npm-7.6.4-421d91b38b-10c0.zip/node_modules/@storybook/nextjs/preset.
Please check whether your setup is correct, the Storybook dependencies (and their peer dependencies) are installed correctly and there are no package version clashes.
If you believe this is a bug, please open an issue on Github.
Error: @storybook/nextjs tried to access webpack (a peer dependency) but it isn't provided by your application; this makes the require call ambiguous and unsound.
Required package: webpack
Required by: @storybook/nextjs@virtual:2e7e27f6cca61c8aa5b8f67bb080d6a92331f92327d9126df87bfe1646b6ea8d8f982aeb8c315f2256aa814646aa2b709c5813c6ddf465d80be2f357cc1b1b56#npm:7.6.4 (via ./.yarn/__virtual__/@storybook-nextjs-virtual-8a558cf2c1/3/.yarn/berry/cache/@storybook-nextjs-npm-7.6.4-421d91b38b-10c0.zip/node_modules/@storybook/nextjs/dist/)
Ancestor breaking the chain: nextjs-styledcomponent-sb@workspace:.
もし、同様なビルドエラーが発生したらこの時点でコミットしておくことをおすすめします。エラーの原因を解消するために各種設定ファイルなどをこねくりまわして泥沼にハマってしまうことがあるので・・・。
ビルドエラーを解消するための追加セットアップ
調査したところ、下記の記事で同じようなパッケージ構成でstorybookが動かない時の対処法を解説されていました。
再度 yarn set version stable
でYarn v4を設定した後、上記記事の通りに.yarnrc.yml
に変更を加えます。
packageExtensions:
"@storybook/nextjs@*":
dependencies:
"@babel/core": "*"
webpack: "*"
追加で下記のパッケージをインストールします。
corepack yarn add -D styled-jsx postcss-import postcss-loader css-loader postcss-url
上記の対応を行ったところ、無事に storybook のビルドが成功しました🎉
Running Storybook
@storybook/cli v7.6.4
info => Starting manager..
info => Starting preview..
info Using Babel compiler
info Addon-docs: using MDX2
info => Using implicit CSS loaders
info => Using default Webpack5 setup
<i> [webpack-dev-middleware] wait until bundle finished
<i> [webpack-dev-middleware] wait until bundle finished: /runtime~main.iframe.bundle.js
<i> [webpack-dev-middleware] wait until bundle finished: /vendors-_yarn_berry_cache_storybook-addon-interactions-npm-7_6_4-82ebd13def-10c0_zip_node_mod-84efbf.iframe.bundle.js
storybook の設定変更
app/components
配下にコンポーネントと一緒に stories ファイルを管理したいと考えたため、.storybook/main.ts
の中身を以下のように書き換えました。Next.js のセットアップ時にTypeScriptを指定しているため、 storybook の設定ファイルも.ts
で作成されています。
const config: StorybookConfig = {
stories: [
"../stories/**/*.mdx",
- "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
+ "../app/**/*.stories.@(js|jsx|mjs|ts|tsx)",
],
コンポーネントと stories ファイルの追加
storybook が機能することを確認するため、app/components
配下にシンプルなボタンを作成し、それの対となる stories ファイルを作成し、読み込ませます。
SimpleButton.tsx
'use client';
import styled from 'styled-components';
const StyledButton = styled.button<{ color: string }>`
background-color: ${({ color }) => color };
color: white;
border-radius: 4px;
padding: 8px;
align-items: center;
&:hover {
cursor: pointer;
}
`;
type ButtonProps = {
label: string;
color?: string;
onClick?: () => void;
};
export const SimpleButton: ButtonProps = ({ label, color = '#3700ff', onClick = () => {} }) => {
return (
<StyledButton color={color} onClick={onClick}>
{label}
</StyledButton>
);
};
stories
storybook v7ではComponentMeta
,ComponentStoryObj
が非推奨になり、Meta
,StoryObj
という型を使うことが推奨されるようになりました。
参考:https://zenn.dev/route06/articles/storybook-v7-deprecations
import React from 'react';
import { StoryObj, Meta } from '@storybook/react';
import { SimpleButton } from './SimpleButton';
const meta: Meta<typeof SimpleButton> = {
component: SimpleButton,
args: {
label: 'Simple Button',
color: '#3700ff',
onClick: () => {}
}
};
export default meta;
type Story = StoryObj<typeof SimpleButton>;
export const Default: Story = {
argTypes: {
onClick: { action: true },
},
}
storybookで確認
最後に、yarn storybook
で再度storybookを開いてコンポーネントのstoriesが作成されているか確認します。
無事にコンポーネントがstorybookで管理できるようになりました🎉
このボイラープレートを使いながら今後の個人開発をスムーズに行っていきたいと思います。
Discussion