Reactのプロダクトにアサインして、実際に触ってみて、自分がわかってなかったことや気付いたことをまとめました!
前提
- 今までの経験: Vue.jsやNuxt.jsを使った業務経験あり。
- ReactとNext.jsの知識: Reactの基本やNext.jsの超基礎(pagesやappディレクトリの使い方)は理解している状態からスタート。
学んだこと・詰まったこと
1. Nuxt.jsとNext.jsのLayoutの考え方の違い
Nuxt.jsでは各ページに完全に独立したレイアウトを、app/layoutディレクトリ配下に定義適用できます。
一方でNext.js(特にApp Routerを使用する場合)は、グローバルレイアウトをベースにしたツリー構造でページを構成します。
例えば、admin/layout.tsx は グローバルの Layout.tsx の中にネストされてしまいます。
何が言いたいかというと、Nextの場合ページごとにLayoutを切り替えるにはひと工夫必要ということです。
この違いを理解するのに初めは混乱の原因になりました。
- Nuxt.js
- layoutプロパティを指定して、ページごとにレイアウトを完全に切り替える。
- 例(app/layout/admin.vue)
- Next.js
- レイアウトはグローバルで一度定義し、その後ディレクトリごとに定義したlayoutはグローバルのlayoutにネストされる
- 例(app/admin/layout.tsxとpage.tsx)
// pages/admin.tsx
import AdminLayout from '@/pages/admin/layout';
export default function AdminPage() {
return <h1>Admin Page</h1>;
}
// `getLayout` を使い、グローバルレイアウトを適用せずに `AdminLayout` のみにする
AdminPage.getLayout = function getLayout(page: React.ReactNode) {
return <AdminLayout>{page}</AdminLayout>;
};
Next.js と Nuxt.js のレイアウト適用方法の比較
項目 | Nuxt.js | Next.js(フォルダごと管理) | Next.js(getLayout 方式) |
---|---|---|---|
レイアウト適用方法 |
layouts/ に事前定義し、layout: '〇〇' で適用 |
layout.tsx をフォルダごとに配置し、ページで import して適用 |
getLayout を各ページに定義し、 _app.tsx で適用 |
レイアウトの切り替え |
layout プロパティを使い、ページごとに指定 |
フォルダごとに layout.tsx を作成し、各ページで明示的にラップ |
getLayout を使えばページごとに動的にレイアウトを適用可能 |
適用の流れ |
layouts/ にレイアウトを作り、ページで layout を指定 |
layout.tsx を作成し、page.tsx で import して適用 |
_app.tsx で getLayout をチェックし、レイアウトを適用 |
グローバルレイアウトとの関係 |
default.vue に定義し、すべてのページに適用 |
✅ ネストされる(_app.tsx の Layout.tsx の中に入る) |
❌ ネストされずに完全に置き換えられる |
柔軟性 |
低い(事前に layouts/ に作らないといけない) |
やや高い(フォルダ単位で分けられるが、グローバルレイアウトにネストされる) | 高い(ページごとにレイアウトを自由に変更できる) |
動的なレイアウト変更 | 不可(事前定義されたレイアウトのみ) | やや可能(フォルダ単位で管理は可能だが、動的適用は手間) |
可能(ページごとに getLayout を定義できる) |
これがNuxtと異なるポイントで、「完全に別のレイアウトを適用したい!」と思った際に苦労しました。
2. React QueryがNext.jsのレイアウトに使われてた
React Queryは、サーバーからデータを取得・キャッシュ・管理するライブラリです。
業務では、Next.jsのレイアウト内で使われており、特定のデータをページ全体で共有する仕組みが構築されていました。
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
このレイアウトに組み込むメリットを調べました
メリット | 詳細 |
---|---|
グローバルデータ管理 | どのページでも同じデータを使える(例: ユーザー情報、設定など) |
API リクエストの最適化 | キャッシュにより、同じデータの不要なリクエストを削減 |
事前フェッチ(Pre-Fetching) | ページ遷移前にデータを取得し、ローディングを減らす |
自動リフレッシュ | 一定間隔でデータを更新(リアルタイム更新に強い) |
エラーハンドリング | 標準でリトライ機能があり、API エラーに強い |
Next.jsのレイアウト内に ReactQueryを組み込むことで、データ管理を効率化し、アプリのパフォーマンスとユーザー体験を向上させれます。
3. Next.js 15ではImageタグのプロパティが変わってた
Next.jsのImageコンポーネントは、パフォーマンス最適化のために使用されます。ただし、バージョン15ではAPI仕様に変更が予定されているとのことです。
例えば、fillプロパティやpriorityが微妙に扱い方が変わっているようです。
新規プロダクトは一部AIで生成されており、Nextのバージョンが最新ではないことがありました。
今後のこのトレンドは続くと思いますが、アップデート時に注意が必要だと感じました
✅変更点
項目 | Next.js 13 以前 | Next.js 13 以降 |
---|---|---|
layout プロパティ |
layout="fill" |
削除され、fill={true} に統一 |
objectFit の指定 |
objectFit="contain" |
style={{ objectFit: 'contain' }} で指定 |
priority の指定 |
priority (なし) |
priority={true} を明示的に設定 |
スタイリング方法 |
objectFit などのプロパティで設定 |
style={{}} を使用 |
推奨記法 |
layout="intrinsic" や layout="responsive"
|
width と height を直接指定するか、fill を使用 |
コード比較
13以前
<Image
src="/logo.png"
alt="Company Logo"
layout="fill"
objectFit="contain"
/>
13以降
<Image
src="/logo.png"
alt="Company Logo"
fill={true}
priority={true}
style={{objectFit: 'contain'}}
/>
4. Reactのチルドレンの型定義って?
コンポーネントを作る際、どんな要素でも許容できるようにspanで設計することがあります。
このspanさせる要素をchildrenと呼びます。
Reactコンポーネントが受け取るchildrenの型定義は、React.ReactNodeを使うのが一般的な方法ですが、構造を知るまで意味がわかりませんでした。
type Props = {
children: React.ReactNode;
};
const MyComponent: React.FC<Props> = ({ children }) => {
return <div>{children}</div>;
};
Reactは、仮想 DOMを構築し、それを元にUIをレンダリングするライブラリです。
この仮想DOMは、ツリー構造を持つデータであり、要素 (div, spanなど) だけでなく、文字列や数値、null などもツリー内に含めることができます。
この ツリー構造を表現するための型 が ReactNode です。
例えば、以下のようなコンポーネントがあります。
const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <div className="wrapper">{children}</div>;
};
export default function App() {
return (
<Wrapper>
<p>Hello, World!</p>
<span>This is a span</span>
</Wrapper>
);
}
この場合、Wrapper の children には次のようなツリー構造のデータが渡されます。
[
{
"type": "p",
"props": { "children": "Hello, World!" }
},
{
"type": "span",
"props": { "children": "This is a span" }
}
]
type ReactNode =
| ReactElement // JSX で記述する要素(単一のDivなどの要素)
| string // 文字列 (テキストノード)
| number // 数値 (テキストノード)
| boolean // true/false(React 内では無視される)
| null // 存在しない要素(React 内では無視される)
| undefined // 存在しない要素(React 内では無視される)
| ReactNode[]; // 配列(複数の子要素を含むことができる)
ReactElementは単一のdiv要素ですが、実際にはdivやh1など複数の要素を子にわたすことが多く、ReactElementでは対応できません。
その際にReactNodeという型であれば、上記のようにいろいろな型を内包しているので対応できコンパイルエラーがおきません。
ように、React.ReactNodeは、string, number, JSX.Elementなど、ほぼすべてのReactで使える子要素を許容する型です。
5. React.FCは使わない?
React.FCをChatgptなどで生成したコードが使っていたので調べてみました。
React.FC(Functional Component)はReactコンポーネントの型になります。
TypeScriptでReactコンポーネントを書くときに便利な型ですが、意外と癖があります。
それはReact.FC を使うと、デフォルトで children を持つことになるからです。
本当はchildrenを受け取らないはずのコンポーネントにも、childrenを渡せてしまいます。
これは意図せぬバグを生みそうで、React18以降では削除されました
const Button: React.FC<{ text: string }> = ({ text }) => {
return <button>{text}</button>;
};
<Button text="クリック" />; // ✅ OK
<Button text="クリック">Hello</Button>; // ❌
結論として、ReactFCを使わずに私は明示的にchildrenを型定義する方法がシンプルなのでいいのではと考えています。
議論されてはいますが、React.FCの明示性のメリットは、現在ではそこまで重要でなさそうです。
//FC使う:コンポーネントの Props の型を一緒に定義
type Props = {
title: string;
};
const MyComponent: React.FC<Props> = ({ title }) => {
return <h1>{title}</h1>;
};
//FC使わない:書き方がよりシンプル
type Props = {
title: string;
};
const MyComponent = ({ title }: Props) => {
return <h1>{title}</h1>;
};
議論されてますね。。
6. &とextendsの違い
TypeScriptで複数の型を組み合わせるとき、&とextendsの違いを理解するのに少し苦労しました。
わかってつもりでわかってなかった。
- &
- (Intersection Type): 型を結合して新しい型を作る。
- extends
- 型を継承して制約を設ける。
記号 | 目的 | ユースケース |
---|---|---|
& (Intersection) |
型を結合(足し算) | Props の拡張、既存の型 + α の型を作る |
extends (継承) |
型の制約・継承 | ジェネリクスで制約、親子関係を作る |
&つかうとき
Props を組み合わせる
type BaseProps = { title: string };
type ExtraProps = { description: string };
type CardProps = BaseProps & ExtraProps; // 2つの型を結合
const Card = ({ title, description }: CardProps) => (
<div>
<h2>{title}</h2>
<p>{description}</p>
</div>
);
HTMLの標準Propsを拡張するときによく使いました。
type CustomButtonProps = { primary?: boolean } & React.ButtonHTMLAttributes<HTMLButtonElement>;
extendつかうとき
特にAPIレスポンスの拡張や親のporpsを継承したいときに便利かと思います。
例えば、追加開発によって、新たなパラメータがついていた際、拡張しやすいからです
function withLogger<T extends BaseProps>(Component: React.ComponentType<T>) {
return (props: T) => <Component {...props} />;
}
7. RadixとShadCNを知る
Radix UIやShadCNは、Reactでアクセシブルかつ再利用可能なUIコンポーネントを提供するライブラリです。
なぜかというと、これがBoltやV0に使われてるからです。
業務でこれらを触り、以下のメリットを感じました:
Radix: アクセシビリティの問題を考慮したUIを簡単に実装可能。
ShadCN: Tailwind CSSと連携しやすく、 Radix を基盤にしたUIコンポーネント集。
特にShadCNでは、forwardRefの使用が頻出しており、その重要性も学びました。
8. ShadCNはなぜforwardRefを使ってるのか?
ShadCNではほぼすべてのコンポーネントがforwardRefを使用しています。
理由は、コンポーネントを外部から拡張可能にするためでした。
子コンポーネントに内包された要素を親からDom操作するためにRefを渡してると理解しています
import React, { forwardRef } from 'react';
const Button = forwardRef<HTMLButtonElement, React.ComponentProps<'button'>>(
(props, ref) => {
return <button ref={ref} {...props} />;
}
);
これにより、refを使って親コンポーネントから直接ボタン要素を操作できます。
9. class-variance-authorityが便利
コンポーネントでclassNameを管理する際、class-variance-authority(CVA)というライブラリを使うと状態に応じたクラス名の管理が簡単になります。
これもshadcnに入ってたので学びました。
無知識でも理解しやすく、カスタマイズとってもしやすかったです。
import { cva } from 'class-variance-authority';
const button = cva('base-class', {
variants: {
color: {
primary: 'text-blue-500',
secondary: 'text-gray-500',
},
},
});
const Button = ({ color }: { color: 'primary' | 'secondary' }) => {
return <button className={button({ color })}>Click Me</button>;
};
クラス名の管理がスッキリして、コードの可読性が向上しますね。
10. Reactの神さまのリポジトリがある
Reactのベストプラクティスを学ぶ際に参考になったのが、Reactの神さまのリポジトリ(非公式表現)でした。
(同僚に教えてもらった)
Shadcnのコンポーネントのコードもこのリポジトリにあったコードとほぼ一緒なので、あくまで予想ですがおそらくこの方が関与しているのは間違いなさそうと思います。
ShadCNがほとんどこの実装でした。
業務で行き詰まったときに非常に助かりました。
おわりに
1ヶ月間React×TypeScript×Nextを触ることで、VueやNuxtとは異なる設計思想やツールの使い方を学びました。
初めは戸惑いもありましたが、学べば学ぶほどその柔軟性とパフォーマンスに魅了されています。
現在JoinしてるプロダクトはVueで書かれています。
Vue自体はとっつきやすくて好きなのですが、
チームで大規模になるとVueの双方向で状態管理できることが裏目にでてきて、デバックが複雑になり、処理を理解するのに苦しんでいます
大規模んあると、3〜4階層以上のコンポーネントになることが多く、その際にpropsとemitsを追ってかないといけないので結構きついです
Reactは一方向の受け渡しのためシンプルで、開発体験いいなと感じております。
AIで吐き出されたアプリケーションもReactが多いので、少しずつ順応していきます!