前回は、退会ページの作成をし、勤怠管理アプリのすべてのページのコンポーネントを作成しました。
ここまでは、ただコンポーネントを作成しただけです。
次は、この作成したコンポーネントを機能させるためのファイルを作成します。
その前に、グローバルナビゲーションは、ユーザー情報があるかないかで表示される内容が異なるので、
ユーザー情報を渡すための関数をfrontendディレクトリ内のsrcディレクトリ内にhooksというディレクトリを作成し、
useAuth.tsというファイルを作成します。
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useMutation } from 'react-query';
import { Me } from '../types';
// APIを呼び出すための関数
const loginUser = async ({ userId, password }: { userId: string; password: string }) => {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId, password }), // JSON形式で送信
});
if (!response.ok) {
throw new Error('ログインに失敗しました。');
}
return response.json();
};
const useAuth = (): {
user: Me | null;
loading: boolean;
errorMessage: string | null;
login: (userId: string, password: string) => void;
} => {
const [user, setUser] = useState<Me | null>(null); // ユーザー情報を保持
const [errorMessage, setErrorMessage] = useState<string | null>(null); // エラーメッセージの保持
const router = useRouter();
const mutation = useMutation(loginUser, {
onMutate: () => {
setErrorMessage(null);
},
onSuccess: (data) => {
setUser(data.user); // 成功時ユーザー情報をセット
},
onError: (error: any) => {
setErrorMessage(error.message || 'ログインに失敗しました。'); // エラーメッセージを表示
},
});
// ログインリクエストを外部から呼び出す関数
const login = (userId: string, password: string) => {
mutation.mutate({ userId, password });
};
// ユーザーが null の場合、ログインページへリダイレクト
useEffect(() => {
if (user === null && router.pathname !== '/login') {
router.push('/login');
}
}, [user, router]);
return {
user,
loading: mutation.isLoading,
errorMessage,
login,
};
};
export default useAuth;
useMutationという
react-query を使用するには、QueryClientProvider を使って QueryClient をコンテキストとしてアプリ全体に渡す必要があります。
QueryClient の作成
const queryClient = new QueryClient();
で QueryClient をインスタンス化します。
これにより、クエリやミューテーションの管理が可能になります。
QueryClientProvider.tsx の作成
まず、React Query の QueryClientProvider を
appディレクトリ内にprovidersディレクトリを作成し、QueryClientProvider.tsxを作成し、そこに定義します。
"use client";
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactNode, useState } from "react";
export default function ReactQueryProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
useState を使うことで QueryClient インスタンスがコンポーネントの再レンダリングごとに作成されるのを防ぎます。
もし、childrenにエラーが出る場合、
tsconfig.jsonファイルを
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
+ },
+ "typeRoots": [
+ "./node_modules/@types",
+ "./node_modules/react-query"
+ ],
+ "types": ["react", "react-dom"] // ここに追加
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
してください。
変更後に node_modules を削除して再インストールしてください。
$ rm -rf node_modules package-lock.json
$ npm install
layout.tsx に QueryClientProvider を適用
次に、layout.tsxを以下のように変更し、 QueryClientProvider をラップ します。
import "../globals.css";
import ReactQueryProvider from "./providers/QueryClientProvider";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "勤怠管理アプリ",
description: "Next.js & Laravel を使った勤怠管理アプリ",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<body>
<ReactQueryProvider>{children}</ReactQueryProvider>
</body>
</html>
);
}
page.tsx で useQuery を使う
page.tsx で useQuery を使うには、クライアントコンポーネント ("use client") にする必要があります。
また、appディレクトリ内にapiディレクトリを作成し、auth.tsファイルを作成します。
そして、API ルートに関するコードを書いていきます。
import { NextApiRequest, NextApiResponse } from "next";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") {
res.status(200).json({ message: "認証API" });
} else {
res.status(405).json({ message: "認証APIエラー" });
}
}
Next.jsを動かした際、ブラウザで、(http://localhost:3000) または、(http://localhost:8080(Dockerを起動させた時)と入力したとき、 エントリーポイントとなるファイルは、frontend/src/app/page.tsxです。
なので、
import Image from "next/image";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
);
}
というコードを変えないといけません。
「▲ Deploy now」という部分だけ残し、書き換えるので、他のボタンや要素は、削除します。
しかし、現在、 <main>
のクラスが sm:items-start
になっているため、sm (小さい画面以上) では左寄せになってしまいます。
なので、 sm:items-start
を削除し items-center
のみにする
<main className="flex flex-col gap-8 row-start-2 items-center">
text-center を追加
リスト (<ol>)
の text-left
が原因で、リスト内の文章が左寄せになっています。
sm:text-left
を削除します。
<ol className="list-inside list-decimal text-sm text-center font-[family-name:var(--font-geist-mono)]">
ボタンを中央寄せ
そして、ボタンの <div>
に sm:flex-row
があるため、小さい画面では縦並び、大きい画面では横並び になっています。
なので、sm:flex-row
を削除しjustify-center
を追加
<div className="flex gap-4 items-center flex-col justify-center">
ボタンの色を青緑色にしたいので、Tailwind CSS の teal 系の色 (teal-500)
を使います。
また、ホバー時に濃い青緑にするために teal-600
を設定します。
bg-foreground text-background
→ bg-teal-500 text-white
hover:bg-[#383838] dark:hover:bg-[#ccc]
→ hover:bg-teal-600
これらとAPIなどを踏まえると、コードを下のように変えていきます。
"use client";
import Image from "next/image";
import { useQuery } from "react-query";
async function fetchData() {
const res = await fetch("/api/auth"); // APIエンドポイントを変更
if (!res.ok) throw new Error("データの取得に失敗しました");
return res.json();
}
export default function Home() {
const { data, error, isLoading } = useQuery("auth", fetchData);
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
{isLoading && <p>読み込み中...</p>}
{error && <p>エラーが発生しました</p>}
{data && <p>取得データ: {JSON.stringify(data)}</p>}
<ol className="list-inside list-decimal text-sm text-center font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
自分で勤怠管理アプリ作ってみました。{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
Next.js and Laravel
</code>
</li>
</ol>
<div className="flex gap-4 items-center flex-col justify-center">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-teal-500 text-white gap-2 hover:bg-teal-600 text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="/login"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
アプリを始める
</a>
</div>
</main>
</div>
);
}
この構成にすれば、全てのページでreact-queryを統一して使用にします。
その後、以下のようにディレクトリとファイルを作成し、作成したコンポーネントを貼り付けていきます。
import { NextPage } from 'next/types';
import Login from '@/components/pages/Login';
import useAuth from '@/hooks/useAuth';
const Index: NextPage = () => {
const { loading, user } = useAuth();
if (loading) {
return <div>処理中...</div>
}
if (!user) {
return <div>ユーザーが見つかりませんでした。</div>
}
return <Login/>;
};
export default Index;
import { NextPage } from 'next/types';
import Leave from '@/components/pages/Leave';
import useAuth from '@/hooks/useAuth';
const Index: NextPage = () => {
const { loading, user } = useAuth();
if (loading) {
return <div>処理中...</div>
}
if (!user) {
return <div>ユーザーが見つかりませんでした。</div>
}
return <Leave/>;
};
export default Index;
import { NextPage } from 'next/types';
import LeaveConfirm from '@/components/pages/Leave/Confirm';
import useAuth from '@/hooks/useAuth';
const Index: NextPage = () => {
const { loading, user } = useAuth();
if (loading) {
return <div>処理中...</div>
}
if (!user) {
return <div>ユーザーが見つかりませんでした。</div>
}
return <LeaveConfirm/>;
};
export default Index;
import { NextPage } from 'next/types';
import LeaveComplete from '@/components/pages/Leave/Complete';
import useAuth from '@/hooks/useAuth';
const Index: NextPage = () => {
const { loading, user } = useAuth();
if (loading) {
return <div>処理中...</div>
}
if (!user) {
return <div>ユーザーが見つかりませんでした。</div>
}
return <LeaveComplete/>;
};
export default Index;
import { NextPage } from 'next/types';
import Mypage from '@/components/pages/Mypage';
import useAuth from '@/hooks/useAuth';
const Index: NextPage = () => {
const { loading, user } = useAuth();
if (loading) {
return <div>処理中...</div>
}
if (!user) {
return <div>ユーザーが見つかりませんでした。</div>
}
return <Mypage/>;
};
export default Index;
import { NextPage } from 'next/types';
import Register from '@/components/pages/Register';
import useAuth from '@/hooks/useAuth';
const Index: NextPage = () => {
const { loading, user } = useAuth();
if (loading) {
return <div>処理中...</div>
}
if (!user) {
return <div>ユーザーが見つかりませんでした。</div>
}
return <Register/>;
};
export default Index;
import { NextPage } from 'next/types';
import Report from '@/components/pages/Report';
import useAuth from '@/hooks/useAuth';
const Index: NextPage = () => {
const { loading, user } = useAuth();
if (loading) {
return <div>処理中...</div>
}
if (!user) {
return <div>ユーザーが見つかりませんでした。</div>
}
return <Report onSubmissionFail={() => {} }/>;
};
export default Index;
import { NextPage } from 'next/types';
import ResetPassword from '@/components/pages/ResetPassword';
import useAuth from '@/hooks/useAuth';
const Index: NextPage = () => {
const { loading, user } = useAuth();
if (loading) {
return <div>処理中...</div>
}
if (!user) {
return <div>ユーザーが見つかりませんでした。</div>
}
return <ResetPassword/>;
};
export default Index;
import { NextPage } from 'next/types';
import TimeTracking from '@/components/pages/TimeTracking';
import useAuth from '@/hooks/useAuth';
const Index: NextPage = () => {
const { loading, user } = useAuth();
if (loading) {
return <div>処理中...</div>
}
if (!user) {
return <div>ユーザーが見つかりませんでした。</div>
}
return <TimeTracking/>;
};
export default Index;
では、作成した勤怠管理アプリを動かしてみます。
Dockerの起動
$ docker-compose up -d –build
そして、ブラウザで、http://localhost:3000 または、http://localhost:8080
と入力して動かしてみると、
というページが、表示されます。
そして、「▲アプリを始める」というボタンを押すと、ログインページに移動します。
しかし、
というエラーが出ているので、
現在、Next.jsやLaravelとのAPI通信ができるように、コードを変更している最中でございます。
進展があれば、随時、更新されるので、都度、確認の方お願いします。
もし、ここまでの記事で、この部分、こうした方がいいやアドバイスなどがあれば、ありがたいです。