FSD で LoginPage をどう分割すべきか
まえがき
Reactでログイン画面を作るとき、解説記事を見ているとページコンポーネントにすべての処理を書くケースはよくあります。
しかし、これらをまとめて 1ファイルに書くと、後からメンテナンスが難しくなるため分割したい。
今回はFSDで構築しているReactアプリでログイン処理を実装したやりかたを解説します。
※この記事は中級者向けです。FSD、カスタムフックあたりを先に勉強していないと意味不明だと思います。
まずはソースコード。
import { MainLayout } from '@/pages/layouts/main'
import { ExecuteButton, Spacer, Subtitle, TextBox } from '@/shared/ui'
import { Container } from '@/shared/ui/container'
import { FC, useState } from 'react'
import { useLogin } from '../model/useLogin'
export const LoginPage: FC = () => {
console.log('▼▼▼ [LoginPage] ▼▼▼')
const [userName, setUserName] = useState<string>('')
const [password, setPassword] = useState<string>('')
const handleSubmit = async (e: React.FormEvent) => {
console.log('▼▼▼ [LoginPage][handleSubmit] ▼▼▼')
e.preventDefault()
const { login } = useLogin()
try {
await login(userName, password)
alert('ログイン成功')
} catch (error) {
alert('ログイン失敗')
}
}
return (
<MainLayout title="ログイン画面">
<Container>
<div>
<Container>
<Subtitle text="" />
<form onSubmit={handleSubmit}>
{/* ユーザ名 (userName) */}
<TextBox
label="ユーザ名"
value={userName}
onChange={(e) => setUserName(e.target.value)}
/>
<Spacer size={20} />
{/* パスワード (password) */}
<TextBox
label="パスワード"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Spacer size={40} />
<ExecuteButton onClick={handleSubmit}>ログイン</ExecuteButton>
</form>
</Container>
</div>
</Container>
</MainLayout>
)
}
import { loginService } from '@/features/backendApi/model/LoginService'
export const useLogin = () => {
const login = async (userName: string, password: string) => {
console.log('▼▼▼ [useLogin][login] ▼▼▼')
const accessToken = await loginService.login(userName, password)
// アプリ側の処理(LocalStorage など)
localStorage.setItem('access_token', accessToken)
return accessToken
}
return { login }
}
import { rootConfig } from '@/shared/config'
class LoginService {
private apiUrl = rootConfig.getApiRootUrl() + '/auth/login'
async login(userName: string, password: string): Promise<string> {
console.log('▼▼▼ [LoginService][login] ▼▼▼')
console.log('API URL:', this.apiUrl)
const res = await fetch(this.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_name: userName,
password: password,
}),
})
if (!res.ok) {
throw new Error('Failed to login')
}
const json = await res.json()
if (!json.access_token) {
throw new Error('access_token が取得できませんでした')
}
return json.access_token
}
}
export const loginService = new LoginService()
1. 切り分け不足の場合の問題点
ページファイルに API 呼び出しとアプリ内部処理をすべて書くと、UI
とロジックが混在して責務が重くなります。
2. API呼び出しだけ切り出してもまだページの責務が重い
とりあえずパッと思いつくのはバックエンドに通信する処理を切り出すこと。
そこでLoginServiceを作成しました。
ですが、LoginService は API 通信のみ担当し、以下は扱わないようにするべきです。
- トークン保存
- エラー処理
- リダイレクト
- context 管理
そのため UI にロジックが残ったままになります。
なので、次のように画面内ロジックをuseLoginとしてさらに切り出します。
3. 推奨される FSD 構成の例
src/
features/
backendApi/
model/
LoginService.ts // API通信を担当
pages/
login/
ui/
page.tsx // HTMLの描画とイベント発火を担当
model/
useLogin.ts // API通信以外のロジックを担当
4. 理由
UI と処理の分離
UI は見た目だけに集中し、ロジックは model に任せる。
ページ肥大化を防ぐ
後からの仕様追加に強い。
API 層を汚さない
LoginService は通信だけに専念。
テストしやすい
層ごとにテストが可能。
FSD の原則に沿う
「pages は薄く」「model にロジック」という基本方針。
5. まとめ
LoginService があっても、UI にロジックが残るため不十分。
FSD では次の三層構造がよさそう。
UI(page.tsx) → ロジック(useLogin.ts) → API(LoginService.ts)
株式会社ONE WEDGE
【Serverlessで世の中をもっと楽しく】 ONE WEDGEはServerlessシステム開発を中核技術としてWeb系システム開発、AWS/GCPを利用した業務システム・サービス開発、PWAを用いたモバイル開発、Alexaスキル開発など、元気と技術力を武器にお客様に真摯に向き合う価値創造企業です。
Discussion