position: sticky
or position: fixed
on element」が出る場合の対応router.push(`/mypage`);
↓
router.push(`/mypage`, { scroll: false });
npm i @nextui-org/react framer-motion
tailwind.config.ts
// tailwind.config.js
const {nextui} = require("@nextui-org/react");
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
......
// ● NextUI この行を追加
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
darkMode: "class",
plugins: [nextui()],
};
・Firebase Console から プロジェクトを作成
・Firestore Database → データベースを作成
・プロジェクトの設定 → ウェブアプリに Firebase を追加 → configを保存。
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
npx create-next-app@latest
npm install firebase
1. src/app/chat/[roomId]/page.tsx
import { ChatRoom } from "@/features/ChatRoom";
type PageProps = {
params: {
roomId: string;
};
};
export default function ChatRoomPage({ params }: PageProps) {
const roomId = params.roomId;
if (!roomId) return <div>error</div>;
return <ChatRoom roomId={roomId} />;
}
2. src/common/firebase/firebaseConfig.ts
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};
const app = initializeApp(firebaseConfig);
export const firestoreDb = getFirestore(app);
3. src/features/ChatRoom.tsx
"use client";
import { firestoreDb } from "@/common/firebase/firebaseConfig";
import {
addDoc,
collection,
onSnapshot,
query,
Timestamp,
} from "firebase/firestore";
import { useRouter } from "next/navigation";
import { FC, useEffect, useState } from "react";
type Message = {
id: string;
text: string;
createdAt: Timestamp;
senderId: string;
username: string;
};
interface ChatRoomProps {
roomId: string;
}
export const ChatRoom: FC<ChatRoomProps> = ({ roomId }) => {
const router = useRouter();
const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState("");
const [username, setUsername] = useState<string>("");
const [currentUser, setCurrentUser] = useState<string | null>(null);
useEffect(() => {
if (!roomId) return;
const q = query(collection(firestoreDb, `chatRooms/${roomId}/messages`));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const msgs: Message[] = [];
querySnapshot.forEach((doc) => {
const data = doc.data();
msgs.push({
id: doc.id,
text: data.text,
createdAt: data.createdAt,
senderId: data.senderId,
username: data.username,
});
});
setMessages(msgs);
});
return () => unsubscribe();
}, [roomId]);
const handleSetUsername = () => {
if (username.trim()) {
setCurrentUser(username);
}
};
const sendMessage = async () => {
if (!newMessage.trim() || !currentUser) return;
await addDoc(collection(firestoreDb, `chatRooms/${roomId}/messages`), {
text: newMessage,
createdAt: Timestamp.fromDate(new Date()),
senderId: "user123", // 実際のユーザーIDに置き換え
username: currentUser,
});
setNewMessage("");
};
return (
<div>
{!currentUser ? (
<div>
<h2>Set your username</h2>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<button onClick={handleSetUsername}>Set Username</button>
</div>
) : (
<div>
<h1>Chat Room: {roomId}</h1>
<div>
{messages.map((msg) => (
<div key={msg.id}>
<p>
<strong>{msg.username}:</strong> {msg.text}
</p>
<span>{msg.createdAt.toDate().toString()}</span>
</div>
))}
</div>
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
/>
<button onClick={sendMessage}>Send</button>
</div>
)}
</div>
);
};
4. src/common/types/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
readonly NEXT_PUBLIC_FIREBASE_API_KEY: string;
readonly NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: string;
readonly NEXT_PUBLIC_FIREBASE_PROJECT_ID: string;
readonly NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: string;
readonly NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: string;
readonly NEXT_PUBLIC_FIREBASE_APP_ID: string;
readonly NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: string;
}
}
5. .env.development
firebase設定を記述します
npm i -D @typescript-eslint/eslint-plugin
.eslintrc.json
{
"extends": [
"prettier",
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended"
],
"rules": {
// img を許可
"@next/next/no-img-element": "off",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
],
// 型の場合 import type に修正してくれる
"@typescript-eslint/consistent-type-imports": "error",
// displayName を省略
"react/display-name": "off"
}
}
npm i react-ga4
src/googleAnalytics/GoogleAnalytics.tsx
'use client';
import ReactGA from 'react-ga4';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
export const GOOGLE_ANALYTICS_ID =
process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID ?? '';
ReactGA.initialize(GOOGLE_ANALYTICS_ID, {
// testMode: true,
});
export const GoogleAnalytics = () => {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (!GOOGLE_ANALYTICS_ID) return;
const url = pathname + searchParams.toString();
// react-ga4:pageview
ReactGA.send({ hitType: 'pageview', page: url });
// react-ga4 : set
// ReactGA.set({ UserID: 123456798 });
}, [pathname, searchParams]);
return null;
};
src/app/layout.tsx
import { GoogleAnalytics } from '@/googleAnalytics/GoogleAnalytics';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<GoogleAnalytics />
<body className={inter.className}>{children}</body>
</html>
);
}
引用 : https://github.com/vercel/next.js/discussions/18301
if you use a custom server, after calling next(), you can reset the asset path at any time
if (assetPrefix) {
nextApp.setAssetPrefix(assetPrefix);
}
npm i -D husky lint-staged
npx husky init
npx husky install
.husky/ディレクトリ内の pre-commit に作成する
echo "npx lint-staged" > .husky/pre-commit
huskyの設定を書ける箇所
1. package.json
2. .huskyrc
3. husky.config.js
4. .husky/ディレクトリ
lint-stagedの設定を書ける箇所
1. package.json
2. .lintstagedrc
package.json
{
"lint-staged": {
"*": "your-cmd"
}
}
.lintstagedrc
{
"*": "your-cmd"
}
concurrent
: デフォルトは true
です。リンターコマンドを並列で実行するかどうかを制御します。false
に設定すると、リンターコマンドは逐次的に実行されます。chunkSize
: 並列処理のチャンクサイズを設定します。デフォルトは maxCpuCount - 1
です。小さな値に設定すると、より多くのプロセスが作成されますが、OOMのリスクが高くなります。globOptions
: グロブパターンのオプションを指定します。たとえば { dot: true }
とすると、ドットファイルも対象に含まれます。ignore
: 無視するパターンを指定します。配列で複数指定できます。linters
: リンターの実行順序を制御します。デフォルトではリンターは並列実行されますが、このオプションで順序付けることができます。matching
: ファイルのマッチングパターンを制御します。デフォルトは ["**/*"]
です。relative
: ワーキングディレクトリを基準にするか、プロジェクトルートを基準にするかを指定します。デフォルトは true
です。shell
: コマンドを実行するシェルを指定します。デフォルトは /bin/sh
です。verbose
: 詳細なログを出力するかどうかを指定します。デフォルトは false
です。subTaskConcurrency
: タスクの並列実行数を指定します。デフォルトは 1
です。src/components/Hello.tsx
const Home = () => {
let unused = 'hello';
return (
<div>
<h1>Hello World</h1>
</div>
);
};
npm run lint
git add -A
git commit
git commit
✖ No valid configuration found.
husky - pre-commit script failed (code 1)
lint-staged は設定したタスク(コマンド)の終了コードが 0 以外の場合、gitコミットをキャンセルする。という手法で動いています。
そこで実行したいタスクの終了コードがエラー時に 0以外を返すかどうかは調べておきましょう。
npx eslint --fix src/MyComponent.tsx; echo "ESLint exit code: $?"
npm i -D @typescript-eslint/eslint-plugin
.eslintrc.json に 吐かせたいルールを追記することで、typescriptエラーも表示させることができます。
{
"extends": [
"plugin:@typescript-eslint/recommended",
"next/core-web-vitals"
],
"rules": {
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-explicit-any": "warn"
}
}
"lint-staged": {
"./**/*.{js,jsx,ts,tsx}": [
"tsc --pretty --noEmit"
],
},
// 1. 型の不一致
const numberVar: number = "this is not a number";
// 2. 未定義の変数の使用
console.log(undeclaredVariable); // 未定義の変数
// 3. 関数のパラメータの型ミスマッチ
function addNumbers(a: number, b: number) {
return a + b;
}
addNumbers(1, "2"); // 第二引数が不正な型
// 4. プロパティが存在しない
const obj = { name: "Alice", age: 30 };
console.log(obj.salary);
// 5. 関数の必須引数の欠如
function greet(name: string) {
return `Hello, ${name}!`;
}
greet();
// 6. インターフェースの実装エラー
interface Person {
name: string;
age: number;
}
const alice: Person = { name: "Alice" };
// 7. ユニオン型の誤用
type StringOrNumber = string | number;
const value: StringOrNumber = true;
┌ ○ / 141 B 84.6 kB
├ λ /[Code]/about 2.16 kB 276 kB
λ (Dynamic) server-rendered on demand using Node.js
↑ この記号が出ているということはダイナミックルートになっています。
こちらを静的ルートに変更してみましょう。
page.tsx に以下を追加します。
export async function getLangNameStaticParams(): Promise<
{ Code: code }[]
> {
return [{ code: "en" }, { code: "ja" }]
}
これで再度ビルドして静的にルートを生成されていることを確認します
---- | App Router | Pages Router |
---|---|---|
APIルートの名前 | Route Handlers | API Routes |
useRouterの場所 | import {useRouter} from "next/navigation" | import { useRouter } from "next/router" |
/app/ ディレクトリ以下では次の命名は特別な意味を持ちます。
layout.tsx |
レイアウト(再レンダリングされない) |
template.tsx |
レイアウト(再レンダリングされるので意図的に際レンダリングしたい場合はこっちを使う) |
page.tsx |
ルーティングファイル(一時的にルートをオフにする場合は ___page.tsx にリネームするなどします) |
loading.tsx |
ローディング コンポーネント |
not-found.tsx |
NotFound コンポーネント |
error.tsx |
React Error Boundary を利用したエラー コンポーネント。 next.js ではデフォルトで以下のようにErrorBoundaryが設定されているので error.tsx を記述するだけでpage.tsxのエラー補足をすることができます。 なので、layout.tsx や template.tsx で発生したエラーの捕捉は error.tsx ではできません |
global-error.tsx |
アプリ全体のエラーを捕捉する。page.tsxと同階層に error.tsx が存在しない場合は global-error.tsx が表示されます Handling Errors in Root Layouts layout.tsx や template.tsx で発生したエラーの捕捉は global-error.tsx でしかできないようです。 global-error.tsx ではコンテキストが受け取れないので注意(error.tsxでやりましょう。) |
route.tsx |
サーバー側 API エンドポイント |
default.tsx |
並列ルートのフォールバック コンポーネント |
src/app/api/hello/route.ts
import { NextResponse, NextRequest } from 'next/server';
export const GET = async () => {
return NextResponse.json(
{ message: 'Hello, Next.js route.ts!' },
{ status: 200 }
);
};
export const POST = async (request: NextRequest) => {
const body = await request.json();
console.log({ body });
// Do something
return NextResponse.json(
{ message: 'Operation successful' },
{ status: 200 }
);
};
import { redirect } from "next/navigation"
redirect("/mypage")
import { notFound } from "next/navigation"
return notFound()
または src/middleware.ts でリダイレクトすることができます。
import { useRouter } from 'next/navigation'
const router = useRouter()
router.push("/mypage")
app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<Link href="/">Return Home</Link>
</div>
)
}
File Conventions: not-found.js | Next.js
画面遷移時に各ページ実行前に middleware で処理を挟み込めます。
src/middleware.ts (注意: src/app/middleware.ts ではありません)
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL("/dashboard-new", request.url))
}
export const config = {
matcher: "/dashboard",
}
matcher: middlewareが動作する対象のパス
src/middleware.ts (注意: src/app/middleware.ts ではありません)
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { currentUser } from "@/firebase/firebaseAuth"
export async function middleware(request: NextRequest) {
return (await isAuthenticated())
? NextResponse.next()
: NextResponse.redirect(new URL("/login", request.url))
}
export const config = {
matcher: "/dashboard",
}
async function isAuthenticated(): Promise<boolean> {
// サーバーサイドでユーザー認証を確認するロジック
}
middlewareはサーバーサイドなので、例えば firebaseのようにユーザーの取得がクライアントサイドの場合は ここに記述せずにクライアント側で記述するか、サーバーでも認証状態を持つ必要があります。
ローカライゼーション
https://zenn.dev/cybozu_frontend/articles/nextjs-i18n-app-router
app/blog/[slug]/page.tsx
この時 /blog/hoge/?page=2 にアクセスした時 hoge や pageを取得したい時は以下のように取得します。
type PageProps = {
params: {
slug: string
}
searchParams: { [key: string]: string | string[] | undefined }
}
export default function Page({
params ,
searchParams,
}: PageProps) {
return <div>My Post: {params.slug}</div>
}
取得した値は以下のようになります。
params = { slug: 'acme' }
searchParams ={ page: '2' }
src/app/layout.tsx
const notoSansJp = Noto_Sans_JP({
weight: ["500"],
subsets: ["latin"],
variable: "--font-noto-sans-jp",
})
return (
<html lang="en">
<body className={notoSansJp.className}>{children}</body>
</html>
)
https://nextjs.org/docs/app/building-your-application/optimizing/metadata
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '...',
description: '...',
}
export default function Page() {}
app/posts/[id]/page.tsx
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
return {
title: `投稿データ{params.id}`,
}
}
app/posts/[id]/page.tsx
// 検索エンジンにインデックスさせない設定
export const metadata: Metadata = {
robots: {
index: false,
follow: false,
// no-cache
nocache: true,
},
}
import { cookies, headers } from 'next/headers';
// cookie
const cookieStore = cookies();
console.log('● cookieStore');
console.log(cookieStore);
// headers
const httpHeaders = headers();
console.log('● httpHeaders');
console.log(httpHeaders);
import MyServerComponent from "./MyServerComponent";
type PostsPageSearchParams = {
page?: string;
sort_by?: string;
sort_order?: "asc" | "desc";
};
type Props = {
params: {};
searchParams: PostsPageSearchParams;
};
export default function Page(props: Props) {
const searchParams = props.searchParams;
console.log(searchParams.pages); // ❌ Error: Property 'pages' does not exist on type 'PostSearchParams'. Did you mean 'page'?
return <MyServerComponent searchParams={searchParams}></MyServerComponent>;
}
こちらがよくまとまっています。
https://dev.classmethod.jp/articles/next-js-image-component/
(タグのcssを設定する場合はグローバルcssに設定しないとエラーとなります。)
app/globals.css (ファイル名は任意。デフォルトでは globals.css)
body {
padding: 60px;
}
app/layout.tsx(ファイル名は任意。デフォルトでは layout.tsx)
import './global.css'
app/hello/page.module.css (ファイル名は任意)
.page_container {
display: grid;
gap: 10px;
}
app/hello/page.tsx(ファイル名は任意)
import css from "./layout.module.css";
jsx の classNameに指定します
<div className={css.page_container}>
hoge
</div>
注意点 ケバブケース ( kebab-case ) は使用できません。
キャメルケースを使用しましょう。
注意点 タグ名は直接使用できません
/* こちらは反映されません */
.page-container div {
background-color: red;
}
https://blog.furu07yu.com/entry/using-route-groups-for-layouts-in-nextjs-13
src/app/providers.tsx
import React, { ReactNode, useState } from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
type ProvidersProps = {
children: ReactNode
}
const Providers: React.FC<ProvidersProps> = ({ children }) => {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
export default Providers
src/app/layout.tsx
// app/layout.jsx
import Providers from './providers'
export default function RootLayout({ children }) {
return (
<html lang="en">
<head />
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
https://x.com/azu_re/status/1760494278965629256?s=20
https://nextjs.org/docs/app/api-reference/next-config-js/logging
next.config.js
module.exports = {
logging: {
fetches: {
fullUrl: true,
},
},
}
参考 : https://speakerdeck.com/mugi_uno/next-dot-js-app-router-deno-mpa-hurontoendoshua-xin
// cacheオプション指定なし
const data = await fetch(url);
// 'no-cache'を指定してもビルド時のfetchは実行されます
const data = await fetch(url, { cache: 'no-cache' });
const data = await fetch(url, { cache: 'no-store' });
または page.tsx や layout.tsx の先頭に
export const dynamic = 'force-dynamic';
と記述すると、ダイナミックページであることが強制されるのでビルド時のfetchも走りません。
こちらもビルド時のfetchは走りません。
page.tsx
export const dynamic = 'force-dynamic';
const FooPage = async () => {
// 'force-cache' となっているが、ビルド時のfetchは走らない
const data = await fetch(url, { cache: 'force-cache' });
.....
https://nextjs.org/docs/app/api-reference/next-config-js/output
next.config.js
/**
* @type { import("next").NextConfig}
*/
const config = {
output: 'standalone'
}
npm run build
node .next/standalone/server.js
HOSTNAME=localhost PORT=3030 node .next/standalone/server.js
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public
npm run build
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public
HOSTNAME=localhost PORT=3000 node .next/standalone/server.js
next/image を使っている場合は、sharp をインストールしてから再度 npm run build します。
npm i sharp
https://nextjs.org/docs/messages/sharp-missing-in-production
sharpをインストールしないと、コンソールにワーニングが出ます。
https://weseek.co.jp/tech/3656/
https://nextjs.org/docs/app/building-your-application/optimizing/images
https://zenn.dev/the_fukui/articles/nextjs-arbitrary-image-path
.next/standaloneディレクトリが作成され、実行に必要な最小限のファイルがコピーされます。Dockerイメージを作成するような場合、この機能によってイメージサイズをかなり縮小できます。
standaloneを設定してyarn buildを行うと.next/standaloneディレクトリ配下に本番環境で必要な最小限のファイル群が集約されて生成されます。
(例外としてpublic,.next/staticディレクトリは.next/standaloneに含まれないため、必要な場合は明示的にCOPYする必要があります。)
ただし.next/staticとpublicはコピーされないため、自分で配置する必要があります。
さらにランタイムにexperimental-edgeを選んでいる場合は注意が必要です。
.next/standalone/serverで必要なファイルが足りない状態となります。
VercelがCDNに入れるべきと判断したファイルはとことん削られています。
それ以外にもnode_modulesの中の*.wasmもコピーされないので、必要とする場合は注意が必要です。
package.json
"scripts": {
"dev": "next dev",
"dev:https": "next dev --experimental-https",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
npm run dev:https
https://localhost:3001 にアクセスできることを確認します。
以上です。簡単ですね。
brew install mkcert
security dump-trust-settings
現時点で何も作成されていなければ、次のように帰ってきます
SecTrustSettingsCopyCertificates: No Trust Settings were found.
mkcert localhost
The certificate is at "./localhost.pem" and the key at "./localhost-key.pem" ✅
npm run build
npm run start
まずは http://localhost:3000/ アクセスできることを確認しておきます。
cd
npx local-ssl-proxy --key localhost-key.pem --cert localhost.pem --source 3001 --target 3000
Started proxy: https://localhost:3001 → http://localhost:3000
https://localhost:3001 にアクセスできることを確認します。 以上です。
sudo vi /etc/hosts
127.0.0.1 my-custom-hostname.local
Macを再起動しておきます。
あとは次のコマンド実行します。
cd
mkcert my-custom-hostname.local
npm run build
npm run start
cd
npx local-ssl-proxy --key my-custom-hostname.local-key.pem --cert my-custom-hostname.local.pem --source 3001 --target 3000
http://my-custom-hostname.local:3000/ への アクセスを確認します
https://my-custom-hostname.local:3001/ への アクセスを確認します
https://next-code-elimination.vercel.app/
左側がページのコンポーネント全て、右側がクライアントに渡されるコードです。
App Router導入後のNext.js開発におけるDead Code Eliminationの活用
isClient を 定義して、これを利用します
const isClient = typeof window !== 'undefined';
t3 stack (trpc) で Date型をそのまま 扱いたい時は superjson を使用すれば そのまま使えますが、 クラスインスタンスを 直接返す時はDate型は文字列にされてしまうので注意しましょう。
具体的には、以下の3つの挙動の違いがあります
・ プレーンオブジェクトの中に Date型のプロパティを含めて返す → ✅ OK
・ クラスインスタンスの中に Date型のプロパティを含めて、クラスインスタンをそのまま返す。 → ❌NG
・ クラスインスタンスの中に Date型のプロパティを含めて class-transformer でプレーンオブジェクトに変換して返す。 → ✅ OK
npm i class-transformer reflect-metadata
export const postRouter = createTRPCRouter({
wordPlain: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return {
id: "1",
name: input.name,
updatedAt: new Date(),
};
}),
wordClass: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
const sampleClass = new SampleClass({
id: "1",
name: input.name,
});
return sampleClass;
}),
wordClassTransform: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
const sampleClassList = [
new SampleClass({
id: "1",
name: `${input.name}_1`,
}),
new SampleClass({
id: "2",
name: `${input.name}_2`,
}),
];
return instanceToPlain(sampleClassList);
}),
const { data: wordPlain } = apiReact.post.wordPlain.useQuery({
name: "wordPlain",
});
const { data: wordClass } = apiReact.post.wordClass.useQuery({
name: "wordClass",
});
const { data: wordClassTransform } =
apiReact.post.wordClassTransform.useQuery({
name: "wordClassTransform",
});
useEffect(() => {
if (!wordPlain) return;
if (!wordClass) return;
console.log("● wordPlain");
console.log(wordPlain);
console.log("● wordClass");
console.log(wordClass);
console.log("● wordClassTransform");
console.log(wordClassTransform);
}, [wordPlain, wordClass, wordClassTransform]);
npm i class-transformer class-validator reflect-metadata
tsconfig.json
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
const nextConfig = {
output: 'standalone',
};
Dockerfile
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT 3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", "server.js"]
このDockerfileはマルチステージビルドです。 マルチステージビルドを行うことで以下のファイル群を最終生成物に含めないようにしています。
1. 依存関係をインストールする際に使用されるパッケージマネージャー自体 (npm、yarn、pnpm など)
2. ビルドツール (webpack、babel など)
3. コンパイル済みのソースコードではなく、アプリケーションのソースコード
4. 各種開発ツール (linter、テストフレームワークなど)
5. パッケージインストーラ関連の一時ファイル (キャッシュディレクトリなど)
.dockerignore
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
docker build -t my-app .
docker run -p 3000:3000 my-app
「Cloud Run を選択」→「サービスを作成 をクリック」
「GitHubのアイコンを選択」→「CloudBuildの設定をクリック」
・リポジトリとブランチを選択
・ビルドタイプは「Dockerfile」を選択
・「コンテナ、ボリューム、ネットワーキング、セキュリティ」をクリックしてコンテナポートを「8080 → 3000」に変更
・「変数とシークレット」タブを選択し、「➕変数を追加」ボタンを押して環境変数を追加します
・CLOUD BUILD へ移動しメニューから「トリガー」を選択
・リストの右側「実行」から「トリガーの実行」
・おそらく上にエラーメッセージが出るのでその中のリンク
https://console.developers.google.com/apis/api/iam.googleapis.com/overview?project=XXXXXXX をブラウザに入力
・Identity and Access Management (IAM) API を「有効にする」
・サイド、先ほどのCLOUD BUILDリストの右側「実行」から「トリガーの実行」を押す
ビルドは 10分程度かかります!待ちましょう!
Firebase Hosting と接続することで・CDN + カスタム ドメインの機能を利用することが可能となります
Cloud Run > 対象のアプリの詳細画面 > 統合(プレビュー) > インテグレーションを追加をクリック
↓
Firebase Hosting をクリック
カスタムドメインを使用すると通信が Client -> ghs.googlehosted.com -> Cloud Run になるのですが ghs.googlehosted.com がアメリカにあります。
Client と Cloud Run が日本にあるとすると接続が 日本→アメリカ→日本 になってしまい遅くなります。
この例では 3600秒(1時間)サーバーサイドでキャッシュが有効になります。
page.tsx
/**
* Route Segment Config
*/
export const revalidate = 3600
export default async function Page() {
fetch('https://...')
unstable_cache を使ってキャッシュ関数を合成することで、キャッシュに対応できるようになります。
公式: https://nextjs.org/docs/app/api-reference/functions/unstable_cache
page.tsx
/**
* Route Segment Config
*/
export const revalidate = 3600
/**
* cached Function
*/
const getUserCached = unstable_cache(
(id:string) => { return getUser(id) } // データ取得関数をここに
[keyParts],
)
export default async function Page() {
const user = getUserCached('xxxxxxxx')
Route Segment Configは以下のデフォルト値を持ちます。
export const dynamic = 'auto'
export const dynamicParams = true
export const revalidate = false
export const fetchCache = 'auto'
export const runtime = 'nodejs'
export const preferredRegion = 'auto'
export const maxDuration = 5
Option | Type | 説明 |
---|---|---|
dynamic | 'auto' | 'force-dynamic' | 'error' | 'force-static' | 'auto' |
dynamicParams | boolean | true |
revalidate | false | 0 | number | false |
fetchCache | 'auto' | 'default-cache' | 'only-cache' | 'force-cache' | 'force-no-store' | 'default-no-store' | 'only-no-store' | 'auto' |
runtime | 'nodejs' | 'edge' | 通常はnodejs。Cloudflareなどにデプロイする場合は edge |
preferredRegion | 'auto' | 'global' | 'home' | string | string[] | 'auto' |
maxDuration | number | サーバーの最大実行時間。これを超えるとエラーとなる。デプロイプラットフォームによって自動設定されます。 |
「アプリ名を入力」→「Website or web app を選択」→「Nextを選択」
を進むと、next.js のアプリ作成初期画面になるので、いつもの通りオプションを選択していく。
あとはデプロイまで自動で行ってくれます。
以下の画面が出れば成功です。
├ SUCCESS View your deployed application at https://YOUR-APP-NAME.pages.dev
│
│ Navigate to the new directory cd YOUR-APP-NAME
│ Run the development server npm run pages:dev
│ Deploy your application npm run pages:deploy
│ Read the documentation https://developers.cloudflare.com/pages
│ Stuck? Join us at https://discord.gg/cloudflaredev
APIを利用する場合は export const runtime = 'edge' をAPIの route.ts に記述する必要があります
例: /src/app/api/helloworld/route.ts
export const runtime = 'edge' // 'nodejs' (default) | 'edge'
エッジランタイムでは Node API は動かないので注意
fsなどの Node依存なAPIは使用できない
使用できるのはWeb標準API(DOMを除くブラウザで操作できるAPI)
手動デプロイ は以下のコマンドです。
npm run pages:deploy
「Workers & Pages」→「Pagesタブを選択」→「Gitに接続ボタンを押す」
あとはメニューに沿ってリポジトリとブランチを選択するだけでokです。
dotenv-cli 、 env-cmd どちらか好きな方を使用しましょう
npm i -D dotenv-cli
npx dotenv -e .env.staging -- next build
.env.production も .env.prod に変更しておいて、明示的に指定してやるとうっかり読み込まれることがなくなるのでおすすめです。
dotenv で .env.prod を指定してビルド
mv .env.production .env.prod
npx dotenv -e .env.prod -- next build
Dockerfile で .env.prod を .env にコピーしてからビルド
RUN COPY .env.prod .env && \
npm run build
npm i -D env-cmd
env-cmd -f .env.staging next build
next.js の appルーターでエンドポイントを1つ作って、あとは trpc の中で分岐させるので作るのもはがすのも簡単です。
├── app
│ └── api
│ └── trpc
│ └── [trpc]
│ └── route.ts (エンドポイント /api/trpc/ を処理する next.js app ルーターの route.ts)
└── trpc
├── client
│ ├── TrpcProvider.tsx
│ ├── client.ts
│ └── serverSideClient.ts
└── server
├── context.ts
├── routers
│ ├── index.ts
│ ├── userRouter.ts (ユーザーに関するルーティング)
│ └── postRouter.ts (投稿に関するルーティング)
└── trpc.ts
npm i @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query@^4.0.0 zod
npm i -D @tanstack/react-query-devtools@4.35.0
src/trpc/server/trpc.ts
import { initTRPC } from "@trpc/server";
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
src/trpc/server/routers/index.ts
import { z } from "zod";
import { publicProcedure, router } from "../trpc";
export const appRouter = router({
helloUser: publicProcedure
.input(
z.object({
userName: z.string(),
}),
)
.mutation(async (opts) => {
return { text: `Hello ${opts.input.userName}` };
}),
hello: publicProcedure.query(async () => {
return { text: "Hello" };
}),
helloText: publicProcedure
.input(
z.object({
text: z.string(),
})
)
.query(async (opts) => {
return { text: `Hello ${opts.input.text}` };
}),
});
export type AppRouter = typeof appRouter;
いわゆる GET は.query() POST は .mutation() になります。
src/trpc/client/client.ts
trpc は reactQuery で使用します。
trpcClient は await を行いたい時など直接リクエストする時に使用します。
import {
createTRPCProxyClient,
createTRPCReact,
httpBatchLink,
} from "@trpc/react-query";
import { type AppRouter } from "../server/routers";
export const trpc = createTRPCReact<AppRouter>({});
export const trpcClient = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: `${process.env.NEXT_PUBLIC_BACKEND_API_BASE_URL}/api/trpc`,
}),
],
});
src/trpc/client/serverSideClient.ts
import { httpBatchLink } from "@trpc/client";
import { appRouter } from "../server";
export const serverSideClient = appRouter.createCaller({
links: [
httpBatchLink({
url: "http://localhost:3000/api/trpc",
}),
],
});
src/trpc/client/TrpcProvider.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import React, { useState } from "react";
import { trpc } from "./client";
export default function TrpcProvider({
children,
}: {
children: React.ReactNode;
}) {
const [queryClient] = useState(() => new QueryClient({}));
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: "http://localhost:3000/api/trpc",
}),
],
}),
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</trpc.Provider>
);
}
src/app/layout.tsx
TrpcProvider を以下のように追加します
import TrpcProvider from "@/trpc/client/TrpcProvider";
return (
<html lang="en">
<body className={inter.className}>
<TrpcProvider>{children}</TrpcProvider>
</body>
</html>
);
クライアントサイドでAPI hello を叩くには trpc.hello.useQuery() を実行します。
src/features/hello/Hello.tsx
"use client";
import { FC } from "react";
import { trpc } from "@/trpc/client/client";
export const HelloComponent: FC = () => {
const { data } = trpc.hello.useQuery();
return (
<div>
<h1>HelloComponent</h1>
<div>{JSON.stringify(data)}</div>
</div>
);
};
引数を受け取る helloText はこのようにして呼び出します。
const { data } = trpc.helloText.useQuery({ text: 'ユーザー名' });
コンポーネントマウント時ではなく、ステートに何か値が入った時にリクエストする場合は次のようにします
const [text, setText] = useState<string>("")
const { data } = trpc.helloText.useQuery(
{ text: text },
{
enabled: text !== "", // text が "" でない場合にのみクエリを有効にする
},
)
// 任意のタイミングで setText() して text に値をセットすると、APIコール発火
どうしても await したい時はこちらの方法も有効です。
import { trpcClient } from "@/trpc/client/client"
const result = await trpcClient.hello({ text:'直接取得' })
サーバーサイドでAPI helloUser を叩くには await serverSideClient.helloUser() を実行します。
src/app/hello-server/page.tsx
import { serverSideClient } from "@/trpc/client/serverSideClient";
const HelloServerPage = async () => {
const data = await serverSideClient.helloUser({
userName: "テスト太郎",
});
return <>{data.text}</>;
};
export default HelloServerPage;
例えば以下のように 独自クラスUser を返す trpcエンドポイントを作成したとします。
getUser: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return new User({
id: "1",
name: input.name,
});
}),
console.log(user)
(updatedAt は Dateオブジェクト)
User {
id: '1',
name: 'server side',
updatedAt: 2024-01-23T05:53:31.525Z
}
console.log(user)
(updatedAt は 文字列)
{
"id": "1",
"name": "hoge fuga",
"updatedAt": "2024-01-23T06:01:58.341Z"
}
https://trpc.io/docs/server/authorization#create-context-from-request-headers
次の手順で認証済みルートを作成します。
- クライアント側で、ログインしていれば { Authorization: `Bearer ${token}` } をしていなければ {} ヘッダを送信するようにします。
- サーバー側で、受け取ったトークンを検証します。(firebase-admin など)
- 「誰でもアクセス可能な publicProcedure」「ログイン済みユーザーのみアクセス可能な protectedProcedure」を作成します。
- 制限をかけたいルートは protectedProcedure を使ってルーティングを定義します。
/api/trpc/user.hello を定義してみます。
import { router, publicProcedure } from '@trpc/server';
// Userサブルーター
const userRouter = router({
hello: publicProcedure.query(async () => {
return { text: "Hello from user" };
}),
});
// メインアプリケーションルーター
export const appRouter = router({
user: userRouter, // userサブルーターを追加
myInfo: publicProcedure.query(async () => {
return { text: "myInfo" };
}),
});
https://trpc.io/docs/server/merging-routers
https://github.com/OrJDev/trpc-limiter/tree/main/packages/memory
もしかすると 'user client' つけ忘れかもしれないので確認しましょう。
'use client'
import { usePathname } from 'next/navigation';
// http://localhost:3001/test1?foo=bar へアクセスした場合 「/test1」 になります
const pathname = usePathname() // /test1
src/app/api/hello/route.ts
import { NextRequest, NextResponse } from "next/server"
type ApiErrorType = {
error: string
message: string
}
type GetResultType = {
message: string
accessDate: string
}
export function GET(
request: NextRequest,
response: NextResponse,
): NextResponse<ApiErrorType> | NextResponse<GetResultType> {
const searchParams = request.nextUrl.searchParams
const name = searchParams.get("name")
if (!name) {
return NextResponse.json(
{
error: "Bad Request",
message: "Parameter 'name' is missing or invalid format",
},
{ status: 400, headers: { "Content-Type": "application/json" } },
)
}
return NextResponse.json({
message: `hello, (${name})!`,
accessDate: new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" }),
})
}
次のエンドポイントを作成します。
(GET) http://localhost:3000/api/trpc/hello
(POST) http://localhost:3000/api/trpc/helloUser 送信する Body「{"userName": "foobar"}」
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query@^4.0.0 zod
1. src/server/trpc.ts を作成します。
import { initTRPC } from "@trpc/server";
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
2. src/server/index.ts を作成します。
import { z } from "zod";
import { publicProcedure, router } from "./trpc";
export const appRouter = router({
hello: publicProcedure.query(async () => {
return { text: "Hello" };
}),
helloUser: publicProcedure
.input(
z.object({
userName: z.string(),
}),
)
.mutation(async () => {
return { text: "Hello" };
}),
});
export type AppRouter = typeof appRouter;
3. src/app/api/trpc/[trpc]/route.ts を作成します。
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server";
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: () => ({}),
});
export { handler as GET, handler as POST };
これだけでOKです。
prisma または drizzle を使用すると良いでしょう。
https://www.prisma.io/docs/concepts/more/comparisons/prisma-and-drizzle
npm i superjson@1
npm i next-superjson-plugin
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
swcPlugins: [["next-superjson-plugin", {}]],
},
}
module.exports = nextConfig
data-superjson を加えて渡します。
page.tsx
export default async function Page({ params }: PageProps) {
const date = new Date()
return (
<MyClientComponent
date={date}
data-superjson
/>
)
}
デフォルトで渡せるオブジェクトは以下のみです
undefined
bigint
Date
RegExp
Set
Map
Error
URL
superjson の registerClass メソッドを使って、独自のクラスのシリアライズ方法とデシリアライズ方法を定義します。
import superjson from 'superjson';
class MyCustomClass {
constructor(public value: string) {}
// このクラス固有のメソッドなど
}
// MyCustomClass をシリアライズおよびデシリアライズする方法を定義
superjson.registerClass(MyCustomClass, {
// オブジェクトをシリアライズする方法
serialize: (instance) => instance.value,
// シリアライズされたデータをオブジェクトにデシリアライズする方法
deserialize: (value) => new MyCustomClass(value),
});
const instance = new MyCustomClass('Hello, World!');
// シリアライズ
const jsonString = superjson.stringify(instance);
// デシリアライズ
const newInstance = superjson.parse(jsonString);
console.log(newInstance); // MyCustomClass { value: 'Hello, World!' }
npm init jest@latest
npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
jest.config.ts
import type { Config } from 'jest'
import nextJest from 'next/jest.js'
const createJestConfig = nextJest({
dir: './',
})
// custom config
const config: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
}
export default createJestConfig(config)
{
moduleNameMapper: {
'^.+\\.module\\.(css|sass|scss)$': '<YOUR-PROJECT-PATH>/node_modules/next/dist/build/jest/object-proxy.js',
'^.+\\.(css|sass|scss)$': '<YOUR-PROJECT-PATH>/node_modules/next/dist/build/jest/__mocks__/styleMock.js',
'^.+\\.(png|jpg|jpeg|gif|webp|avif|ico|bmp)$': '<YOUR-PROJECT-PATH>/node_modules/next/dist/build/jest/__mocks__/fileMock.js',
'^.+\\.(svg)$': '<YOUR-PROJECT-PATH>/node_modules/next/dist/build/jest/__mocks__/fileMock.js',
'@next/font/(.*)': '<YOUR-PROJECT-PATH>/node_modules/next/dist/build/jest/__mocks__/nextFontMock.js',
'next/font/(.*)': '<YOUR-PROJECT-PATH>/node_modules/next/dist/build/jest/__mocks__/nextFontMock.js',
'server-only': '<YOUR-PROJECT-PATH>/node_modules/next/dist/build/jest/__mocks__/empty.js'
},
testPathIgnorePatterns: [ '/node_modules/', '/.next/' ],
transform: {
'^.+\\.(js|jsx|ts|tsx|mjs)$': [
'<YOUR-PROJECT-PATH>/node_modules/next/dist/build/swc/jest-transformer.js',
[Object]
]
},
transformIgnorePatterns: [ '/node_modules/', '^.+\\.module\\.(css|sass|scss)$' ],
watchPathIgnorePatterns: [ '/.next/' ]
}
npm install -D jest @types/jest ts-jest
jest.config.js
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
}
jest.setup.js
require("dotenv").config({ path: ".env.development" })
sizuhiko - Technote - ts-jest が esbuild/swc をトランスフォーマーに使って高速化していた
process.title で判別します
function isBrowser(){
return process.title === 'browser'
}
process.title の戻り値
// ブラウザ の場合
"browser"
// nodejs の場合
"/PATH/TO/YOR/NODEJS/versions/16.0.0/bin/node"
swcMinify:true があると効かないようです。
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
// swcMinify: false, // ● この行は必ず削除しましょう
}
module.exports = nextConfig
content の リストの中で使用されているクラスがビルド時に使用できるようになるので、もし反映されないコンポーネントが リストにない場合はそこに追加しましょう。
tailwind.config.ts
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/features/**/*.{js,ts,jsx,tsx,mdx}', // ● /features/ 以下を追加する場合はこのように追加します
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}'
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))'
}
}
},
plugins: []
}
export default config
import { useEffect, useState } from 'react'
import Image from 'next/image'
export const FallbackImage = ({ src, ...rest }) => {
const [imgSrc, setImgSrc] = useState(src)
useEffect(() => {
setImgSrc(src)
}, [src])
return (
<Image
{...rest}
src={imgSrc ? imgSrc : '/images/not-found.png'}
onError={() => {
setImgSrc('/images/not-found.png')
}}
/>
)
}
sample.module.css
.btnRed {
background-color: red;
color: white;
}
コンポーネントでの使用
import styles from "@/styles/common.module.css";
return (
<button className={styles.btnRed}>ボタン</button>
)
npm i sass -D
あとはファイル名を sample.module.scss のように拡張子 .scss にするだけです。
コンポーネントでの使用
import styles from "@/styles/common.module.css";
return (
// 存在しないクラス .hogehoge でエラーが出ない
<button className={styles.hogehoge}>ボタン</button>
)
typed-scss-modulesのインストール
npm i typed-scss-modules -D
vi typed-scss-modules.config.ts
以下の内容で保存します
export const config = {
exportType: 'default',
nameFormat: 'none',
implementation: 'sass'
}
型生成コマンド
npx typed-scss-modules src
happy-css-modules のインストール
npm i -D happy-css-modules
型生成コマンド
npx hcm 'src/**/*.module.{css,scss,less}'
src/types/env.d.ts ( ファイル名やディレクトリはどこでもokです )
declare namespace NodeJS {
interface ProcessEnv {
readonly APP_NAME: string;
readonly NEXT_PUBLIC_APP_NAME: string;
readonly DB_CONNECTION: string;
readonly DB_HOST: string;
readonly DB_PORT: string;
readonly DB_DATABASE: string;
readonly DB_USERNAME: string;
readonly DB_PASSWORD: string;
}
}
.env
# APP
APP_NAME=アプリ名
NEXT_PUBLIC_APP_NAME=${APP_NAME}
npm install --save-dev npm-run-all opener
package.json
"scripts": {
"dev": "npm-run-all --parallel dev:next dev:browser",
"dev:next": "next dev -p 3999",
"dev:browser": "sleep 1 && opener http://localhost:3999/",
},
実行
npm run dev
npm-run-allのオプション
順次実行
「npm-run-all --serial」または「npm-run-all -s」または「run-s」と記述します
並列実行
「npm-run-all --parallel」または「npm-run-all -p」または「run-p」と記述します
新たに作成するアプリの場合は emotionのかわりに goober を使うという手もあります。
https://github.com/cristianbote/goober
npx create-next-app@latest
( app router を選択してアプリを新規作成します )
npm install @emotion/react
{
"compilerOptions": {
// emotion
"jsx": "preserve",
"jsxImportSource": "@emotion/react",
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
emotion: true,
},
};
export default nextConfig;
いずれも先頭に以下を追加
/* @jsxImportSource react */
クライアントコンポーネントの先頭に以下を追加
'use client';
以上で動作します!
npm run dev
components/SampleCss.tsx
'use client';
import { css } from '@emotion/react';
import { FC } from 'react';
export const SampleCss: FC = () => {
return (
<div
css={css`
color: green;
font-size: 48px;
font-weight: bold;
`}
>
SampleCss
</div>
);
};
import { css } from '@emotion/react';
const fontLarge = css`
font-size: 48px;
`;
const colorGreen = css`
color: green;
`;
export default function Home() {
return (
<>
<div css={[fontLarge, colorGreen]}>home</div>
</>
);
}
import { css } from '@emotion/react';
const fontLarge = css`
font-size: 48px;
`;
const myFont = css`
${fontLarge}
color: blue;
`;
export default function Home() {
return (
<>
<div css={myFont}>home</div>
</>
);
}
import type { AppProps } from 'next/app';
import { css, Global } from '@emotion/react';
const globalStyle = css`
body {
font-family: 'Helvetica Neue', Arial, 'Hiragino Kaku Gothic ProN',
'Hiragino Sans', Meiryo, sans-serif;
}
`;
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Global styles={globalStyle}></Global>
<Component {...pageProps} />
</>
);
}
src/pages/index.tsx
import { useState } from 'react';
import { css } from '@emotion/react';
type MyFont = {
colored:boolean
}
export default function Home() {
const [colored, setColored] = useState<boolean>(false);
const myFont = ({ colored }:MyFont) => css`
border: solid 1px black;
border-radius: 10px;
padding: 16px;
cursor: pointer;
${colored &&
`
background-color: #413F42;
color: white;
`}
`;
return (
<>
<button
onClick={() => {
setColored(!colored);
}}
>
色変更
</button>
<div css={myFont({ colored: colored })}>home</div>
</>
);
}
npm install @emotion/styled
こちらで css から js object に 変換します
https://transform.tools/css-to-js
1. SampleStyled.tsx(styled.div記法)
'use client';
import styled from '@emotion/styled';
const MyTitle = styled.div`
border: 10px solid blue;
padding: 2rem;
font-size: 3.5rem;
margin: 0px;
color: blue;
`;
export function SampleStyled() {
return <MyTitle as={'button'}>home</MyTitle>;
}
as={'button'} で 出力時にタグを div から button に変更しています
2. SampleStyled.tsx(className渡し記法)
import styled from '@emotion/styled';
import { FC, ComponentProps } from 'react';
type StylableFC = FC<ComponentProps<'div'>>;
const SampleStyled: StylableFC = ({ className }) => {
return <div className={className}>コンポーネントサンプル</div>;
};
export default styled(SampleStyled)`
margin: 16px 0;
background-color: #f6f6f6;
color: green;
padding: 20px;
border-radius: 8px;
font-size: 64px;
font-weight: bold;
`;
react17 で emotionを動かすには
1. tsconfig.json
"jsx": "preserve",
"jsxImportSource": "@emotion/react",
2. コンポーネントファイルの先頭に jsxプラグマを追加
/** @jsxImportSource @emotion/react */
3. コンポーネントファイルの先頭に jsxプラグマを追加したくない場合はこちら
https://qiita.com/y-suzu/items/2d3fcf5414f7b418f05a
https://emotion.sh/docs/@emotion/babel-preset-css-prop
https://qiita.com/xrxoxcxox/items/17e0762d8e69c1ef208f
@emotion/reactでのスタイル指定方法 - Qiita
npm i zustand
とても便利なので、こちらから入れておきましょう
https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ja
src/stores/book.ts
import { create } from "zustand";
type BookStoreData = {
amount: number;
title: string;
};
const initialData: BookStoreData = {
amount: 0,
title: "",
};
interface BookStore {
book: BookStoreData;
updateAmount: (newAmount: number) => void;
updateTitle: (newTitle: string) => void;
fetchTitle: () => void;
}
export const useBookStore = create<BookStore>((set, get) => ({
book: { ...initialData },
updateAmount: (newAmount: number) => {
const amountState = get().book.amount;
const newBook: BookStoreData = {
amount: newAmount + amountState,
title: get().book.title,
};
set({ book: newBook });
},
updateTitle: (newTitle: string) => {
const titleState = get().book.title;
const newBook: BookStoreData = {
amount: get().book.amount,
title: newTitle,
};
set({ book: newBook });
},
fetchTitle: async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
const newBook: BookStoreData = {
amount: get().book.amount,
title: "fetched Title",
};
set({ book: newBook });
},
}));
ストア値の参照 : const book = useBookStore((state) => state.book);
アクションfetchTitle の呼び出し : const fetchTitle = useBookStore((state) => state.fetchTitle);
const BookSample = () => {
const book = useBookStore((state) => state.book);
const updateAmount = useBookStore((state) => state.updateAmount);
const updateTitle = useBookStore((state) => state.updateTitle);
const fetchTitle = useBookStore((state) => state.fetchTitle);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
fetchTitle();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetchTitle]);
return (
<div>
<h1> BookTitle: {book.title} </h1>
<h1> Books: {book.amount} </h1>
<button onClick={() => updateAmount(10)}> Update Amount </button>
<div>
<input type="text" ref={inputRef} />
<button
onClick={() =>
updateTitle(inputRef.current ? inputRef.current.value : "")
}
>
Update Text
</button>
</div>
</div>
);
};
( SSR は Firebase Functionsへ。)
npx create-next-app@latest --use-npm <アプリ名>
https://console.firebase.google.com/ からプロジェクトを作成します。プロジェクト名はアプリ名と同じとしておくと良いでしょう。
以下をそれぞれの場所に追記します。
{
"main": "firebaseFunctions.js",
"scripts": {
"serve": "npm run build && firebase emulators:start --only functions,hosting",
"shell": "npm run build && firebase functions:shell",
"deploy": "firebase deploy --only functions,hosting",
"logs": "firebase functions:log"
},
"dependencies": {
"firebase-admin": "^9.4.2",
"firebase-functions": "^3.13.1",
},
"devDependencies": {
"firebase-functions-test": "^0.2.3",
"firebase-tools": "^9.3.0"
}
}
その後にパッケージをインストールします
npm install
vi .firebaserc
{
"projects": {
"default": "<アプリ名>"
}
}
vi firebaseFunctions.js
const { https } = require('firebase-functions')
const { default: next } = require('next')
const nextjsDistDir = './.next/';
const nextjsServer = next({
dev: false,
conf: {
distDir: nextjsDistDir,
},
})
const nextjsHandle = nextjsServer.getRequestHandler()
exports.nextjsFunc = https.onRequest((req, res) => {
return nextjsServer.prepare().then(() => nextjsHandle(req, res))
})
vi firebase.json
{
"hosting": {
"public": "public",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"function": "nextjsFunc"
}
]
},
"functions": {
"source": ".",
"predeploy": [
"npm --prefix \"$PROJECT_DIR\" install",
"npm --prefix \"$PROJECT_DIR\" run build"
],
"runtime": "nodejs16"
},
"emulators": {
"functions": {
"port": 5001
},
"hosting": {
"port": 5002
},
"ui": {
"enabled": true
},
"singleProjectMode": true
}
}
npm run serve
・プランを Blaze にアップグレードする
・Firebase Hosting を開始する
npm run deploy
Project ID が 正しくない可能性があります。以下のコマンドからプロジェクトIDを確認します。
firebase projects:list
プロジェクトIDが間違っている場合は、次のファイル内のプロジェクトIDを正しいものに書き変えます。 .firebaserc
なお、SSR は Firebase Functions としてデプロイされるのでSSR可能です。
例: アプリ名 my-test-hosting-app としています
npx create-next-app@latest --use-npm --example with-firebase-hosting my-test-hosting-app
プロジェクト名はアプリ名と同じ my-test-hosting-app とするとわかりやすいです。 プロジェクト名はコピーしてクリップボードに保存しておきます
アプリのルートディレクトリに移動して、そこからコマンドでログインします。(間違いがないように先にログアウトしておきます)
firebase logout
firebase login
.firebaserc を変更する
{
"projects": {
"default": "先ほど保存したプロジェクト名をここにペースト"
}
}
プロジェクト名をコピーし、忘れた時は、次のコマンドで一覧を表示させて、そこからコピーします
firebase projects:list
firebase.json
ポートを 5002番に変更します
emlators を追加します
{
"hosting": {
........
} ,
"functions": {
........
} ,
"emulators": {
"functions": {
"port": 5001
},
"hosting": {
"port": 5002
},
"ui": {
"enabled": true
},
"singleProjectMode": true
}
}
package.json には
"serve": "npm run build && firebase emulators:start --only functions,hosting",
があるので このスクリプトを実行します。
npm run serve
アプリ(ローカル)
http://localhost:5002/
firebase コンソール(ローカル)
http://localhost:4000/
functions の node.js のバージョンを16に設定します
firebase.json
"functions": {
........
"runtime": "nodejs16"
},
npm run deploy
Firebase コンソールの歯車アイコン → プロジェクトの設定 → サービスアカウント → 新しい秘密鍵の生成
で秘密鍵をダウンロードします。
Githubでアプリのリポジトリへ移動 → settings → Secrets and variables → Actions →「New repository secret」
Name : FIREBASE_SERVICE_ACCOUNT_<FirebaseプロジェクトIDを大文字で。>
Secret : ダウンロードした秘密鍵のJSONを貼り付ける
Githubでアプリのリポジトリへ移動 → Settings → Actionsの中のGeneral → General → Workflow permissions を以下の画像のように設定する
GitHub CLI のインストール
brew install gh
gh secret list
vi .github/workflow/firebase-hosting-merge.yml
npm install -g firebase-tools
firebase --version
firebase logout
firebase login
firebase init hosting
npm install firebase
npm i server-only
import "server-only";
を 先頭に記述します。 これをクライアントで描画すると以下のようなエラーがスローされます。
(サーバーサイドで実行された時にエラーがスローされます)
import "client-only";
/ja/ または ロケールなし の場合は 日本語
/en/ の場合は 英語
とするには以下のように記述します
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
i18n: {
locales: ["en", "ja"],
defaultLocale: "ja",
},
};
module.exports = nextConfig;
これで
http://localhost:3000/login の場合は日本語
http://localhost:3000/ja/login の場合は日本語
http://localhost:3000/en/login の場合は英語
となります。
import { usePathname } from 'next/navigation';
const getLocale = (path: string): string => {
if (path.match(/^\/en/)) return 'en';
return 'ja';
};
const pathname = usePathname();
const locale = getLocale(pathname);
i18nにおけるロケールの表は、主にISO 639言語コードとISO 3166国コードに基づいて決定されます。
ISO 639言語コードは、言語を識別するための2〜3文字のコードです。たとえば、英語のISO 639コードは "en" であり、日本語のISO 639コードは "ja" です。
一方、ISO 3166国コードは、国または地域を識別するための2文字または3文字のコードです。たとえば、アメリカ合衆国のISO 3166コードは "US" であり、日本のISO 3166コードは "JP" です。
これらのコードを組み合わせることで、i18nのロケールコードが形成されます。たとえば、英語を話すアメリカ人のためのロケールは、"en_US"と表記されます。同様に、日本語を話す日本人のためのロケールは、"ja_JP"と表記されます。
ただし、ISO規格以外のロケールコードも存在する場合があります。それらは、一般的な業界規格、アプリケーション固有の規則、あるいは地域の文化的および言語的な違いに基づくものがあります。
import { useRouter } from "next/router";
const { pathname } = useRouter()
import { useRouter } from "next/router";
const { asPath } = useRouter()
import { useRouter } from 'next/router';
const router = useRouter();
const docId = router.query.id; // 123456
http://localhost:3002/test?page=12
/pages/ 内の .tsx ファイルのgetServerSideProps()にてURLパラメーターを取得する方法です。 これだと props で コンポーネントに渡されるので router.isReady を待つ必要がありません。
/src/pages/test.tsx
import { MyComponent } from '@/components/MyComponent';
import { GetServerSidePropsContext, NextPage } from 'next';
interface Props {
page: number;
}
export async function getServerSideProps(context: GetServerSidePropsContext):Promise<{props:Props}> {
const { page: queryPage } = context.query;
const page = queryPage ? Number(queryPage) : 999;
return {
props: {
page,
},
};
}
const Test: NextPage<Props> = (props ) => {
return (
<>
<h1>Hello Test</h1>
<h5>{props.page}</h5>
<MyComponent page={props.page} />
</>
);
};
export default Test;
location /app/ {
proxy_pass http://localhost:3000/;
}
assetPrefix を追加します
const SUB_DIRECTORY = "/app";
const isProduction = process.env.NODE_ENV === "production";
/** @type {import('next').NextConfig} */
const nextConfig = {
assetPrefix: isProduction ? SUB_DIRECTORY : "/" ,
reactStrictMode: true,
swcMinify: true,
};
module.exports = nextConfig;
next.config.js
module.exports = {
assetPrefix: '/hoge'
};
next.config.js
const isProduction = process.env.NODE_ENV === "production";
module.exports = {
assetPrefix: isProduction ? '/app' : '/'
};
npx create-next-app@latest --ts cypress-testing-app
cd cypress-testing-app
npm install cypress --save-dev
npm install @testing-library/cypress --save-dev
"scripts": {
......
"cy:open": "cypress open",
"cy:run": "cypress run"
},
あらかじめ実行しておきます
npm run dev
最初に一度起動します
npm run cy:run
cypress\e2e\0-my-tests\0-my-sample.cy.js
describe('example to-do app', () => {
it('ルートパスに訪問できるか', () => {
cy.visit('http://localhost:3000/')
})
})
npm run cy:open
npm run cy:run
middlewareはサーバサイドです
/src/middleware.ts
import { NextRequest, NextResponse } from 'next/server';
const isClient = () => typeof window !== 'undefined';
export const middleware = (req: NextRequest) => {
console.log('isClient ?');
console.log(isClient());
return NextResponse.next();
};
import Link from 'next/link'
Next.js version 13以降
<Link href="/about">About Us</Link>
Next.js version 12以前
<Link href="/about"><a>About Us</a></Link>
useRouter を使用する(こちらがおすすめです)
import { useRouter } from 'next/router';
const router = useRouter();
if (router.isReady) {
router.push({
pathname: '/login',
query: { returnUrl: router.asPath }
})
}
Router
import Router from 'next/router';
Router.push('/home'); // '/home'へ遷移
useRouter は hooks なので、 実際にルーターのインスタンス を取得したときにre-render されるので以下のコードが正しく実行できます。 Routerの場合は まだインスタンスがないので実行できません。
○ OK
import { useRouter } from "next/router";
const router = useRouter();
if (router.isReady) {
router.push("/login");
}
× NG
import Router from "next/router";
if (Router.isReady) {
Router.push("/login");
}
<button onClick={() => router.back()}>
戻る
</button>
nextjsアプリの初期化
npx create-next-app@latest --ts sample-app
cd sample-app
パッケージのインストール
npm install --save typescript ts-node
npm install --save typeorm sqlite3
npm install sqlite3 --save
npm install reflect-metadata --save
npm install @types/node --save
yarn typeorm init --database sqlite3
ormconfig.json が 自動生成されますので、以下のように追記します。
"type": "sqlite",
"database": "data/dev.sqlite",
{
"type": "sqlite",
"database": "data/dev.sqlite",
"synchronize": true,
"logging": false,
"entities": [
"src/entity/**/*.ts"
],
"migrations": [
"src/migration/**/*.ts"
],
"subscribers": [
"src/subscriber/**/*.ts"
],
"cli": {
"entitiesDir": "src/entity",
"migrationsDir": "src/migration",
"subscribersDir": "src/subscriber"
}
}
/src/entity/Post.ts
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
export class Post {
@PrimaryGeneratedColumn()
readonly id: number;
@Column('varchar', { length: 255, nullable: false })
name: string;
@Column('int', { nullable: true })
sort_no: string;
@CreateDateColumn()
readonly created_at?: Date;
@UpdateDateColumn()
readonly updated_at?: Date;
}
Windowsのターミナルから
node_modules\.bin\ts-node ./node_modules/typeorm/cli.js migration:generate -n Post
node_modules\.bin\ts-node ./node_modules/typeorm/cli.js migration:run
と実行します
https://www.wakuwakubank.com/posts/730-typeorm-custom-naming/
elastomer_appま
yarn typeorm init
yarn ts-node node_modules/.bin/typeorm migration:show
エンティティ Post の マイグレーションファイルを自動生成する
yarn ts-node node_modules/.bin/typeorm migration:generate -n Post
src/migration/1647220932735-Post.ts といった命名のファイルが自動生成されます
yarn ts-node node_modules/.bin/typeorm migration:run
https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading
import { MyComp } from "../components/MyComp";
↓
import dynamic from "next/dynamic";
const MyCompNoSSR = dynamic(() => import("./MyComp"), { ssr: false });
以上で、SSRが回避されます。
import MyComp from "../components/MyComp";
↓
import dynamic from "next/dynamic";
const MyCompNoSSR = dynamic(
() => import("./MyComp").then((modules) => modules.MyComp),
{ ssr: false },
);
以上です。
このように 呼び出されるコンポーネント側に記述することもできます
import App from '../components/App'
export default function About() {
return (
<App>
<p>About Page</p>
</App>
)
}
↓
import dynamic from 'next/dynamic'
import App from '../components/App'
const About = ()=> {
return (
<App>
<p>About Page</p>
</App>
)
}
export default dynamic(() => Promise.resolve(About), {
ssr: false
})
以上で、SSRが回避されます。
ページをリロードしてhtmlソースを見てみます。
<p>About Page</p>
がなければ、SSRされていません。
React18以上が必要です。必ずバージョンを確認しましょう
const DynamicLazyComponent = dynamic(() => import('../components/hello4'), {
suspense: true,
})
https://nextjs.org/docs/advanced-features/dynamic-import
npx create-next-app@latest my-app
npx create-next-app@latest --ts my-app
NEXT JS アプリのビルドを高速化させるターボパックを追加するには with-turbopack オプションを追加します
npx create-next-app@latest my-app --ts with-turbopack
turbopack でビルドを行うには次のコマンドを実行します
next dev --turbo
npx create-next-app -e with-tailwindcss my-project
-e オプションはこちらのリポジトリからデータを持ってきます https://github.com/vercel/next.js/tree/master/examples
公式のリポジトリにサンプルがないためまずTypeScriptでアプリを作成してその後にTailWindを追加します
npx create-next-app@latest --ts my-app
cd my-app
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
module.exports = {
mode: 'jit',
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
import 'tailwindcss/tailwind.css';
return (
<div className="text-red-500 text-4xl sm:text-6xl lg:text-7xl leading-none font-extrabold tracking-tight mt-10 mb-8 sm:mt-14 sm:mb-10">テストです</div>
)
IntelliSense for CSS class names in HTML を無効にしましょう
変更前の _app.tsx
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
↓ 変更後の _app.tsx
import "@/styles/globals.css";
import { ReactElement, ReactNode } from "react";
import { NextPage } from "next";
import type { AppProps } from "next/app";
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactElement) => ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
export default function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout =
Component.getLayout ||
((page) => {
return page;
});
return getLayout(<Component {...pageProps} />);
}
メインの children のところが各ページ内容に置き換わります
mkdir components
vi components/Layout.tsx
components/Layout.tsx
import React from "react";
interface Props {
children?: React.ReactNode;
}
const SimpleLayout: React.FC<Props> = ({ children }: Props) => {
return (
<>
<h1>header</h1>
{/* ===== メイン ===== */}
<main>{children}</main>
{/* ===== /メイン ===== */}
<h1>footer</h1>
</>
);
};
export default SimpleLayout;
変更前の src/pages/mypage/index.tsx
import { Mypage } from '@/features/mypage/Mypage'
import LoggedInLayout from '@/layouts/LoggedInLayout'
import { ReactNode } from 'react'
import { NextPageWithLayout } from '@/pages/_app'
const PagesMypage: NextPageWithLayout = () => {
return <Mypage />
}
PagesMypage.getLayout = (page: ReactNode) => {
return <LoggedInLayout>{page}</LoggedInLayout>
}
export default PagesMypage
使うのは以下の2ファイルに限定すると良いでしょう。
.env.development : 開発用ファイル
NODE_ENV が development ( = npm run dev )の時に読み込まれる。
.env.production : 本番用ファイル
NODE_ENV が production ( = npm run build && npm run start )の時に読み込まれる。
書き方(サーバーサイド)
( .env.development または .env.production)
HOGE=mySettingValue
呼び出し方(サーバーサイド)
( xxx.js や xxx.ts ファイル )
console.log( process.env.HOGE );
書き方(フロントエンド)
( .env.development または .env.production)
NEXT_PUBLIC_HOGE=mySettingValue
呼び出し方(フロントエンド)
( xxx.js や xxx.ts ファイル )
console.log( process.env. NEXT_PUBLIC_HOGE );
NODE_ENV で判断すると良いでしょう
console.log( process.env.NODE_ENV );
typeof window === "undefined"
windowオブジェクトがないのがサーバーサイド
windowオブジェクトがあるのがフロントエンド
です。
デバッグ用にサーバーがクライアントを返したい場合はメソッドにしておいても良いかもです
export default function ServerOfClient() {
return (typeof window === "undefined") ? 'Server' : 'Client';
}
Booleanの場合はそのまま利用しても良いですし以下のようにしても良いです
const isProduction = process.env.NODE_ENV === "production";
console.error(`● process.env.APOLLO_FETCH_POLICY は (${process.env.APOLLO_FETCH_POLICY}) / NODE_ENVは(${process.env.NODE_ENV}) / Server or Client は(${ServerOfClient()})`);
結果例
● process.env.APOLLO_FETCH_POLICY は (network-only-S) / NODE_ENVは(development) / Server or Client は(Server)
control + c でいちどプロセスを終了してから再度起動します
npm run dev