CSR × SSRを用いたサイトの実装
Published on December 19, 2024
Category: Next.js
実現したいこと
page.tsx上にcomponetsを参照してStateなどを管理できるよようにしたい(SSR内でCSRを用いて実装する。)
なぜやるのか
- SSRではサーバー側でレンダリングが実行されるため、初期の表示が高速で実装することができる。
- CSRを用いることで、SSRでレンダリングされているページの一部を動的に更新して、不要なページ全体のリロード・再レンダリングが行わくてよい。
実装前提
- page.tsxをSSRで実装
- サーバー側でレンダリングが実行されるため、初期表示の高速化することができる。
- componentをCSRとして扱う。
- UIはあくまでもユーザーが操作するもの
- 操作により状態が変化するため、ユーザーの操作によって状態を変化させる必要があるため、CSRで定義してあげる。
- UIはあくまでもユーザーが操作するもの
実装
0. 考え方
デザインパターン:Presentation/Container Componentの考え方を参考に、UIとビジネスロジックの関心ごとを切り分けて実装をする。
1. ディレクトリ構造
- appディレクトリ
- アプリケーションとしてのビジネスロジックの振る舞いを持つ。
- app/hooks
- ビジネスロジックを実装
- app/auth/signup/_components
- コンポーネントとhooksを組み合わせて実装して、page.tsxに参照してあげる。
- page.tsxはSSRで実装しているため、CSRのコンポーネントを参照して、hooksで定義しているものを渡すことができないため、このディレクトリを実装している。
- packages
- uiディレクトリ
- コンポーネントとしての振る舞いのみを持つ。
- コンポーネントのスタイルやコンポーネントそのものの定義を持つ。
- uiディレクトリ
root/
├── app/
│ ├── api/
│ ├── auth/
│ │ └── signup/
│ │ ├── _components/
│ │ │ └── signup-form.tsx
│ │ └── page.tsx
│ ├── hooks/
│ │ └── signup/
│ │ └── index.ts/
│ ├── layout.tsx
│ ├── page.tsx
│ └── globals.css
│
└── packages/
└── ui/
└── components/
├── atoms/
│ ├── button/
│ └── input/
├── molecules/
│ └── form-field/
├── organisms/
│ └── form/
├── templates/
└── pages/
2. 処理の実行フロー

3. 実装詳細
このコードは CSR の実装を検証・動作確認を目的とした実装
確認ポイント
- ボタンクリック時のクライアントサイドでの状態変更
- ローディング状態に応じたUIの動的な更新
- useStateの フックを使用したステート管理の動作
動作確認詳細
- 「送信」ボタンをクリック
- ボタンのテキストが「送信中」に変わることを確認
- 2秒後に「送信」に戻ることを確認
これにより、Reactコンポーネントがクライアントサイドで正しく動作し、状態更新とレンダリングが適切に行われていることを確認できる。
4. 実装コード
// app/auth/signup/page.tsx
'use server';
import Signup from './_component/signup';
function SignupPage() {
return (
<div className="flex justify-center items-center min-h-screen from-gray-500 via-gray-600 to-gray-700 h-80 w-96 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 absolute">
<Signup />
</div>
);
}
export default SignupPage;
// app/auth/signup/_component/signup/index.tsx
'use client';
import { FormComponent } from './../../../../../../../packages/ui/src/components/organisms/form';
import { useSignup } from './../../../../hooks/signup';
export default function Signup() {
const { isLoading, handleSignup } = useSignup();
return (
<FormComponent
title={{ title: 'タイトル' }}
formField={{
formField: [
{
label: 'メールアドレス',
input: { placeholder: 'メールアドレスを入力してください' },
},
{
label: 'パスワード',
input: { placeholder: 'パスワードを入力してください' },
},
],
}}
className="h-80 w-96"
onSubmit={handleSignup}
isLoading={isLoading}
/>
);
}
// app/hooks/signup/index.ts
use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
export function useSignup() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const handleSignup = async (event: React.MouseEvent<HTMLButtonElement>) => {
alert('実行されました - isloading: ' + isLoading);
event.preventDefault();
// 遅延処理を実行する
alert('遅延処理が実行');
setIsLoading(true);
setTimeout(() => {
alert('遅延処理中です');
setIsLoading(false);
}, 1000);
};
return {
handleSignup,
isLoading,
};
}
// packages/ui/src/components/atoms/button/index.tsx
import React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/src/lib/utils';
import { Button } from '@/src/shadcn/button';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
},
},
}
);
type ButtonProps = VariantProps<typeof buttonVariants> &
React.ComponentProps<typeof Button>;
export const ButtonComponent = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant, className, onClick, ...props }, ref) => {
return (
<Button
variant={variant}
className={cn(buttonVariants({ variant }), className)}
ref={ref}
{...props}
onClick={onClick}
/>
);
}
);
// packages/ui/src/components/organisms/form/index.tsx
'use client';
import React from 'react';
import { cn } from '@/src/lib/utils';
import { cva, type VariantProps } from 'class-variance-authority';
import { CardComponent } from '@/src/components/atoms/card';
import { CardTitleComponent } from '@/src/components/atoms/cardTitle';
import { FormFieldComponent } from '@/src/components/molecules/formField';
import { ButtonComponent } from '@/src/components/atoms/button';
const formVariants = cva('block');
type FormProps = VariantProps<typeof formVariants> & {
title: React.ComponentProps<typeof CardTitleComponent>;
formField: {
formField: {
label: string;
input: {
placeholder: string;
};
}[];
};
className?: string;
onSubmit?: (event: React.MouseEvent<HTMLButtonElement>) => void;
isLoading?: boolean;
};
export const FormComponent = ({
title,
formField,
className,
onSubmit,
isLoading,
...props
}: FormProps) => {
return (
(
<CardComponent className={cn(formVariants(), className)}>
<CardTitleComponent {...title} />
{formField.formField.map((field, index) => (
<FormFieldComponent key={index} formField={field} />
))}
<ButtonComponent onClick={onSubmit}>
{isLoading ? '送信中' : '送信'}
</ButtonComponent>
</CardComponent>
)
);
};