はじめに
私はClaude4.0を利用して個人開発アプリの作成に挑戦しています。
個人開発でReactアプリを作っていた時、「ユーザーにとって親切な機能を作ろう!」と思い、useEffectを使った自動初期化機能に挑戦しました。
しかし結果的に、実装が複雑になりすぎて自分でも理解ができなくなり、最終的にシンプルなボタン実装に戻すことになりました。
この経験から学んだ「適切な複雑さ」と「useEffectの正しい使い方」について振り返ります。
何を作ろうとしたのか?
目標:親切なユーザー体験
「アプリを開いたら即座に使える状態にしたい」という思いから、以下の動作を目指しました:
// 理想の動作フロー
1. アプリ起動
2. 自動的に認証状態をチェック
3. 未ログイン → 自動でゲストユーザー作成
4. すぐにアプリが使える + ログインを促すUI表示
これらの機能は確かに使いやすいのですが、裏側の実装は相当複雑だということを軽視していました。
実装された複雑なコード
useEffectを使った自動初期化
function App() {
const [userData, setUserData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [showAuthChoice, setShowAuthChoice] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const initializeApp = async () => {
try {
setIsLoading(true);
// 1. 認証状態チェック
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
if (sessionError) {
throw sessionError;
}
if (session?.user) {
// 2. 認証済みユーザーの処理
const authUserData = await fetchUserProfile(session.user.id);
setUserData(authUserData);
} else {
// 3. 未認証 → 自動ゲストユーザー作成
const deviceId = await DeviceIdManager.getDeviceId();
const guestUser = createGuestUser(deviceId);
setUserData(guestUser);
setShowAuthChoice(true); // ログイン促進UI表示
}
} catch (error) {
console.error('初期化エラー:', error);
setError(error);
// エラー時のフォールバック処理
const fallbackUser = createFallbackUser();
setUserData(fallbackUser);
} finally {
setIsLoading(false);
}
};
initializeApp();
}, []); // 空の依存配列
// ローディング中
if (isLoading) {
return <LoadingSpinner />;
}
// エラー状態
if (error) {
return <ErrorComponent error={error} />;
}
return (
<div>
<QuizComponent userData={userData} />
{showAuthChoice && (
<AuthPrompt
onLogin={() => loginWithGoogle()}
onClose={() => setShowAuthChoice(false)}
/>
)}
</div>
);
}
なぜuseEffectが使われたのか?
まず、なぜ「普通に関数内に書くだけ」ではダメなのかを理解する必要があります。
Reactの根本的な仕組み:関数全体が何度も実行される
function App() {
const [count, setCount] = useState(0);
// 「初期化処理だから1回だけ実行されるはず」と思い込み
console.log('初期化処理が実行されました');
const userData = createUser(); // 毎回新しいユーザーが作られる!
console.log('作成されたユーザー:', userData.id);
return (
<div>
<p>Count: {count}</p>
<p>User: {userData.id}</p>
<button onClick={() => setCount(count + 1)}>
カウントアップ
</button>
</div>
);
}
// 実際のコンソール出力:
// 初期化処理が実行されました
// 作成されたユーザー: user_001
// ↓ ボタンを1回クリック
// 初期化処理が実行されました ← また実行された!
// 作成されたユーザー: user_002 ← 別のユーザーID!
// ↓ ボタンをもう1回クリック
// 初期化処理が実行されました ← またまた実行された!
// 作成されたユーザー: user_003 ← また別のユーザーID!
問題点:
- Reactコンポーネントは「関数」(Appコンポーネントが関数)
- 状態が変わるたびに関数全体が再実行される
- 「初期化のつもり」の処理も毎回実行される(1回だけの実行になってくれない!)
useEffectで「本当に1回だけ」を保証
function App() {
const [count, setCount] = useState(0);
const [userData, setUserData] = useState(null);
// この部分は何度も実行される
console.log('関数が実行された');
// useEffectの部分は「1回だけ」を保証
useEffect(() => {
console.log('初期化処理:本当に1回だけ実行');
const newUser = createUser();
setUserData(newUser);
}, []); // 空配列 = マウント時のみ実行
return (
<div>
<p>Count: {count}</p>
<p>User: {userData?.id}</p>
<button onClick={() => setCount(count + 1)}>
カウントアップ
</button>
</div>
);
}
// 実行結果:
// 関数が実行された
// 初期化処理:本当に1回だけ実行 (User: user_abc123)
// ↓ ボタンクリック
// 関数が実行された (User: user_abc123) ← 同じユーザー!
// (初期化処理は実行されない)
なぜ「1回だけ」が重要なのか
// ユーザー作成が毎回実行されると...
❌ API呼び出しが何度も発生 → サーバー負荷
❌ 新しいユーザーIDが毎回生成 → データの整合性が破綻
❌ 初期化処理が重い場合 → パフォーマンス悪化
❌ ユーザーが混乱 → 「なぜIDが変わる?」
// useEffectで1回だけに制限すると...
✅ 初期化は最初の1回のみ
✅ 状態変更時は初期化処理は実行されない
✅ 予期した通りの動作
useEffectを選んだ意図:
- アプリ起動(コンポーネントマウント)と同時に自動初期化
- でも絶対に1回だけ実行させたい
- ユーザーが何もしなくても使える状態にする
- 「体験までの時間を最短にする」親切設計
問題点:複雑さが招いた混乱
1. 状態管理が複雑すぎた
const [userData, setUserData] = useState(null); // ユーザーデータ
const [isLoading, setIsLoading] = useState(true); // ローディング状態
const [showAuthChoice, setShowAuthChoice] = useState(false); // UI表示制御
const [error, setError] = useState(null); // エラー状態
// 状態の組み合わせパターンが爆発的に増加
// loading + error + userData + showAuthChoice = 16通りの状態
2.エラーハンドリングが困難
// どこでエラーが起きるかわからない
try {
const session = await supabase.auth.getSession(); // エラー1
const profile = await fetchUserProfile(); // エラー2
const deviceId = await DeviceIdManager.getDeviceId(); // エラー3
const guestUser = createGuestUser(); // エラー4
} catch (error) {
// どのエラーなのか判別困難
console.error('どこかでエラー:', error);
}
3.デバッグが大変
// useEffect内の非同期処理はデバッグしにくい
useEffect(() => {
const initializeApp = async () => {
// この中でエラーが起きても、どのタイミングかわからない
// コンソールログを大量に仕込む羽目に...
console.log('1. 開始');
console.log('2. セッション取得中...');
console.log('3. ユーザー作成中...');
// ...
};
initializeApp();
}, []);
シンプルな解決策:ボタンクリック実装
結局、元のシンプルな実装に戻しました:
// ✅ シンプルで理解しやすい実装
function App() {
const [userData, setUserData] = useState(null);
// Google認証
const handleGoogleLogin = async () => {
try {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google'
});
if (error) throw error;
} catch (error) {
console.error('ログインエラー:', error);
alert('ログインに失敗しました');
}
};
// ゲストログイン(ボタンクリック時のみ)
const handleGuestLogin = () => {
const guestUser = createGuestUser();
setUserData(guestUser);
};
// 未ログイン状態:選択画面を表示
if (!userData) {
return (
<div className="login-screen">
<h1>学習アプリへようこそ</h1>
<button onClick={handleGoogleLogin}>
Googleでログイン
</button>
<button onClick={handleGuestLogin}>
ゲストとして始める
</button>
</div>
);
}
// ログイン済み:アプリ本体
return <QuizComponent userData={userData} />;
}
シンプル実装のメリット
1. 理解しやすい
// 動作が直感的
ボタンクリック → 関数実行 → 状態更新 → 画面更新
2.デバッグしやすい
// エラーの原因が特定しやすい
const handleGuestLogin = () => {
console.log('ゲストログイン開始');
const guestUser = createGuestUser(); // ← ここでエラーなら一目瞭然
console.log('ゲストユーザー作成完了:', guestUser);
setUserData(guestUser);
};
3.エラーハンドリングが明確
// どの処理でエラーが起きたか一目瞭然
const handleGoogleLogin = async () => {
try {
await googleLogin();
} catch (error) {
console.error('Googleログインエラー:', error);
// 具体的なエラー対応
}
};
const handleGuestLogin = () => {
try {
const user = createGuestUser();
setUserData(user);
} catch (error) {
console.error('ゲストユーザー作成エラー:', error);
// 具体的なエラー対応
}
};
useEffectを使うべき適切な場面
useEffectは素晴らしい機能ですが、「1回だけ実行したい処理」がある時に使うものです。使いどころを間違えると複雑になります:
✅ useEffectが適切な場面
1. 初期データの取得(1回だけ)
useEffect(() => {
// アプリ起動時に1回だけユーザー設定を取得
fetch('/api/user-settings')
.then(res => res.json())
.then(data => setSettings(data));
}, []); // 空配列 = 1回だけ
2.設定の読み込み(1回だけ)
function App() {
const [theme, setTheme] = useState('light');
useEffect(() => {
// アプリ起動時に1回だけ保存されたテーマを読み込み
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme);
}
}, []); // 1回だけ実行
return (
<div className={theme === 'dark' ? 'dark-mode' : 'light-mode'}>
アプリ画面
</div>
);
}
3.初期化処理(1回だけ)
function QuizApp() {
const [questions, setQuestions] = useState([]);
useEffect(() => {
// アプリ起動時に1回だけクイズ問題を準備
const initialQuestions = prepareQuizQuestions();
setQuestions(initialQuestions);
}, []); // 1回だけ実行
return <div>クイズが{questions.length}問あります</div>;
}
今回の実装は、アプリ起動時に1回だけ実行したい場合という条件には合っていましたが、初心者には難し過ぎて扱い切れませんでした。
学んだこと:「適切な複雑さ」を見極める
1. ユーザー体験 vs 実装コスト
// 理想的なUX:自動初期化
実装コスト: 高い(複雑な状態管理、エラーハンドリング)
保守コスト: 高い(デバッグ困難、テスト困難)
// シンプルなUX:ボタンクリック
実装コスト: 低い(明確な処理フロー)
保守コスト: 低い(理解しやすい、テストしやすい)
// 個人開発ではまずは「シンプル」からステップアップすべき
2. 段階的な実装
// ✅ 推奨アプローチ
Phase 1: まずシンプルなボタン実装で動作確認
Phase 2: ユーザーフィードバックを収集
Phase 3: 本当に必要なら高度な機能を追加
// ❌ 避けるべきアプローチ
Phase 1: いきなり完璧な自動化を目指す
→ 複雑すぎて挫折
3. 「親切」の定義を見直す
// 思い込んでいた「親切」
= 自動的に何でもやってくれる
// 実際のユーザーにとって「親切」
= 予測しやすい動作
= エラーが起きたときの分かりやすさ
= 自分で制御できる感覚
まとめ
今回の経験で学んだポイント:
useEffectについて
- 複雑な初期化処理には向かない
- 単純なデータフェッチやイベント設定に最適
- 非同期処理が多いと状態管理が困難になる
実装設計について
- 「親切すぎる」機能は実装コストが高い
- シンプルな実装から始めて段階的に改善
- ユーザー体験と実装コストのバランスを考慮
個人開発の心得
- 完璧を目指さず、動くものを先に作る
- 複雑さは段階的に追加する
- 「なぜその機能が必要か?」を常に自問
「技術的にできること」と「やるべきこと」は違う、ということを改めて学んだ貴重な経験でした。
IT技術学習アプリ
私が作成に取り組んでいる「IT技術学習アプリ」です。React等の技術について学習できるアプリになっています。
現在は開発中のベータ版ですが、周辺技術も学習しながら継続して改善中です。