プレイグラウンド、サンドボックス、使い捨てスクリプト置き場
0

Configure Feed

Select the types of activity you want to include in your feed.

update

Kohei Watanabe (Jan 13, 2026, 3:07 PM +0900) cb880e86 c657e0c6

+139 -43
+139 -43
use-swr/src/App.tsx
··· 4 4 5 5 const API_BASE = "https://jsonplaceholder.typicode.com"; 6 6 7 - const fetcher = (url) => fetch(url).then((r) => r.json()); 7 + async function fetcher(url) { 8 + const res = await fetch(url); 9 + if (!res.ok) throw new Error(`HTTP Error: ${res.status}`); 10 + return res.json(); 11 + } 8 12 9 13 // ======================================== 10 14 // Providers: SWRのグローバル設定 ··· 32 36 style={{ 33 37 border: "1px solid #ccc", 34 38 padding: "1rem", 35 - margin: "0.5rem", 36 39 minWidth: "200px", 37 40 }} 38 41 > ··· 59 62 const [enableRefresh, setEnableRefresh] = useState(false); 60 63 61 64 return ( 62 - <section style={{ marginBottom: "2rem" }}> 63 - <h2>1. キャッシュ共有 + 定期更新</h2> 65 + <section style={{ display: "flex", flexDirection: "column", gap: "1rem" }}> 66 + <h2 style={{}}>1. キャッシュ共有 + 定期更新</h2> 64 67 <p style={{ fontSize: "0.9rem", color: "#666" }}> 65 68 同じURLを参照する2つのコンポーネントがキャッシュを共有。 66 69 <br /> ··· 72 75 display: "flex", 73 76 gap: "1rem", 74 77 alignItems: "center", 75 - marginBottom: "1rem", 76 78 }} 77 79 > 78 - <label> 80 + <label style={{ display: "flex", alignItems: "center", gap: "1rem" }}> 79 81 ユーザーID: 80 82 <select 81 83 value={userId} 82 84 onChange={(e) => setUserId(Number(e.target.value))} 83 - style={{ marginLeft: "0.5rem" }} 84 85 > 85 86 {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((id) => ( 86 87 <option key={id} value={id}> ··· 100 101 </label> 101 102 </div> 102 103 103 - <div style={{ display: "flex", flexWrap: "wrap" }}> 104 + <div style={{ display: "flex", flexWrap: "wrap", gap: "1rem" }}> 105 + <UserDetail userId={userId} enableRefresh={enableRefresh} /> 106 + <UserDetail userId={userId} enableRefresh={enableRefresh} /> 104 107 <UserDetail userId={userId} enableRefresh={enableRefresh} /> 105 108 <UserDetail userId={userId} enableRefresh={enableRefresh} /> 106 109 <UserDetail userId={userId} enableRefresh={enableRefresh} /> ··· 142 145 ); 143 146 144 147 return ( 145 - <section style={{ marginBottom: "2rem" }}> 146 - <h2>2. キーの先行切り替え</h2> 148 + <section style={{ display: "flex", flexDirection: "column", gap: "1rem" }}> 149 + <h2 style={{}}>2. キーの先行切り替え</h2> 147 150 <p style={{ fontSize: "0.9rem", color: "#666" }}> 148 151 IDを変更すると、IDは即座に切り替わるがデータは後から到着。 149 152 <br /> 150 153 SWRのキー変更でリクエストが発生する様子を確認できる。 151 154 </p> 152 155 153 - <div style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}> 156 + <div style={{ display: "flex", gap: "1rem" }}> 154 157 {[1, 2, 3, 4, 5].map((id) => ( 155 158 <button 156 159 key={id} ··· 171 174 172 175 <div 173 176 style={{ 177 + display: "flex", 178 + flexDirection: "column", 179 + gap: "0.5rem", 174 180 border: "1px solid #ccc", 175 181 padding: "1rem", 176 182 minHeight: "100px", ··· 181 187 display: "flex", 182 188 alignItems: "center", 183 189 gap: "1rem", 184 - marginBottom: "0.5rem", 185 190 }} 186 191 > 187 192 <span ··· 211 216 </div> 212 217 ) : ( 213 218 <div> 214 - <p style={{ margin: 0 }}> 219 + <p style={{}}> 215 220 <strong>{data?.name}</strong> 216 221 </p> 217 - <p style={{ margin: 0, fontSize: "0.9rem", color: "#666" }}> 218 - {data?.email} 219 - </p> 222 + <p style={{ fontSize: "0.9rem", color: "#666" }}>{data?.email}</p> 220 223 </div> 221 224 )} 222 225 </div> ··· 225 228 } 226 229 227 230 // ======================================== 228 - // Profile: 条件付きフェッチ 231 + // Profile: 条件付きフェッチ + エラーハンドリング 229 232 // - フォーム入力が完了するまでフェッチしない 230 233 // - 空文字の場合はkeyがnullになりリクエストが発生しない 234 + // - mutate() を使ったエラー回復とダミーデータ作成 231 235 // ======================================== 232 236 function Profile() { 233 237 // 入力中のユーザーID 234 238 const [userId, setUserId] = useState(""); 235 239 // 確定したユーザーID 236 240 const [submittedUserId, setSubmittedUserId] = useState(""); 241 + // エラー時の自動リトライ(手動リトライするときは無効化する) 242 + const [shouldRetryOnError, setShouldRetryOnError] = useState(false); 237 243 238 - const { data, error, isLoading } = useSWR<{ 239 - id: number; 240 - name: string; 241 - email: string; 242 - phone: string; 243 - }>(submittedUserId ? `${API_BASE}/users/${submittedUserId}` : null); 244 + const { data, error, isLoading, mutate } = useSWR( 245 + submittedUserId ? `${API_BASE}/users/${submittedUserId}` : null, 246 + { shouldRetryOnError }, 247 + ); 248 + 249 + // 存在しないユーザーをローカルで「作成」する(mutateでキャッシュに直接書き込む) 250 + const createDummyUser = () => { 251 + mutate( 252 + { 253 + id: Number(submittedUserId), 254 + name: `ダミーユーザー ${submittedUserId}`, 255 + email: `dummy${submittedUserId}@example.com`, 256 + phone: "000-0000-0000", 257 + }, 258 + { revalidate: false }, // サーバーに再リクエストしない 259 + ); 260 + }; 244 261 245 262 return ( 246 - <section style={{ marginBottom: "2rem" }}> 247 - <h2>3. 条件付きフェッチ</h2> 263 + <section style={{ display: "flex", flexDirection: "column", gap: "1rem" }}> 264 + <h2 style={{}}>3. 条件付きフェッチ + エラーハンドリング</h2> 248 265 <p style={{ fontSize: "0.9rem", color: "#666" }}> 249 266 IDを入力して「検索」を押すまでリクエストは発生しない。 250 267 <br /> 251 - (DevTools Networkで確認) 268 + 存在しないID(11以上)を入力するとエラーが発生 → mutateで回復可能。 252 269 </p> 270 + <label> 271 + <input 272 + type="checkbox" 273 + checked={shouldRetryOnError} 274 + onChange={(e) => setShouldRetryOnError(e.target.checked)} 275 + /> 276 + エラー時に自動再試行 277 + </label> 253 278 254 279 <form 255 280 onSubmit={(e) => { 256 281 e.preventDefault(); 257 282 setSubmittedUserId(userId.trim()); 258 283 }} 259 - style={{ marginBottom: "1rem" }} 284 + style={{ display: "flex", gap: "1rem" }} 260 285 > 261 286 <input 262 287 type="text" 263 288 value={userId} 264 289 onChange={(e) => setUserId(e.target.value)} 265 - placeholder="ユーザーID (1-10)" 266 - style={{ padding: "0.5rem", marginRight: "0.5rem" }} 290 + placeholder="ユーザーID (1-10, 11以上はエラー)" 291 + style={{ padding: "0.5rem", width: "220px" }} 267 292 /> 268 293 <button type="submit" style={{ padding: "0.5rem 1rem" }}> 269 294 検索 ··· 274 299 <p style={{ color: "#888" }}>IDを入力して検索してください</p> 275 300 )} 276 301 277 - {isLoading && <p>読み込み中...</p>} 278 - {error && ( 279 - <p style={{ color: "red" }}>エラー: ユーザーが見つかりません</p> 302 + {isLoading && <p style={{}}>読み込み中...</p>} 303 + 304 + {error && !isLoading && ( 305 + <div 306 + style={{ 307 + display: "flex", 308 + flexDirection: "column", 309 + gap: "0.5rem", 310 + background: "#fee", 311 + border: "1px solid #f99", 312 + borderRadius: "8px", 313 + padding: "1rem", 314 + }} 315 + > 316 + <p style={{ color: "#c00" }}> 317 + <strong>ユーザー {submittedUserId} が見つかりません</strong> 318 + </p> 319 + <p style={{ color: "#666", fontSize: "0.9rem" }}> 320 + このIDのユーザーはサーバーに存在しません。 321 + </p> 322 + <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}> 323 + <button 324 + onClick={() => mutate()} 325 + style={{ 326 + background: "#007bff", 327 + color: "#fff", 328 + border: "none", 329 + padding: "0.5rem 1rem", 330 + borderRadius: "4px", 331 + cursor: "pointer", 332 + }} 333 + > 334 + 再試行 335 + </button> 336 + <button 337 + onClick={createDummyUser} 338 + style={{ 339 + background: "#28a745", 340 + color: "#fff", 341 + border: "none", 342 + padding: "0.5rem 1rem", 343 + borderRadius: "4px", 344 + cursor: "pointer", 345 + }} 346 + > 347 + ユーザーを作成 348 + </button> 349 + </div> 350 + <p style={{ color: "#888", fontSize: "0.8rem" }}> 351 + 「ユーザーを作成」は mutate() でキャッシュに直接データを書き込みます 352 + </p> 353 + </div> 280 354 )} 281 - {data && ( 355 + 356 + {data && !error && ( 282 357 <div style={{ border: "1px solid #ccc", padding: "1rem" }}> 283 358 <p> 284 359 <strong>{data.name}</strong> 285 360 </p> 286 361 <p>{data.email}</p> 287 362 <p>{data.phone}</p> 363 + <button 364 + onClick={() => mutate()} 365 + style={{ 366 + background: "#007bff", 367 + color: "#fff", 368 + border: "none", 369 + padding: "0.5rem 1rem", 370 + borderRadius: "4px", 371 + cursor: "pointer", 372 + }} 373 + > 374 + 再試行 375 + </button> 288 376 </div> 289 377 )} 290 378 </section> 291 379 ); 292 380 } 293 381 294 - // ======================================== 295 - // App: メインコンポーネント 296 - // ======================================== 297 382 function App() { 298 383 return ( 299 384 <Providers> 300 - <div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}> 301 - <h1>SWR 学習サンプル</h1> 302 - <p style={{ marginBottom: "2rem", color: "#666" }}> 303 - 各セクションでSWRの機能を体験できます。 304 - <br /> 305 - Chrome DevTools &gt; Network で挙動を確認してください。 306 - </p> 385 + <div 386 + style={{ 387 + display: "flex", 388 + flexDirection: "column", 389 + gap: "2rem", 390 + maxWidth: "800px", 391 + margin: "0 auto", 392 + padding: "2rem", 393 + }} 394 + > 395 + <header> 396 + <h1>SWR 学習サンプル</h1> 397 + <p style={{ color: "#666" }}> 398 + 各セクションでSWRの機能を体験できます。 399 + <br /> 400 + Chrome DevTools &gt; Network で挙動を確認できます。 401 + </p> 402 + </header> 307 403 308 404 <User /> 309 405 <UserSwitcher />