# Lab 13: React #3 - useEffect Hook & API Integration

## 🎯 วัตถุประสงค์

เมื่อเสร็จสิ้น Lab นี้ นิสิตสามารถ

- เข้าใจ useEffect Hook และวงจรชีวิต Component
- สร้าง Side Effects ต่างๆ (Fetching Data, Timers, Event Listeners)
- จัดการ Dependency Array ให้ถูกต้อง
- ใช้ Cleanup Function เพื่อป้องกัน Memory Leaks
- ดึงข้อมูลจาก API และแสดงผล
- จัดการ Loading, Error, Success states
- ป้องกัน Race Conditions และ Stale Closure

---

## 📋 ส่วนที่ 1: useEffect Basics

### ขั้นตอน 1.1: เข้าใจ useEffect คืออะไร

useEffect ใช้สำหรับ Side Effects นอกเหนือจาก render:

```javascript
import { useEffect } from "react";

export default function App() {
  useEffect(() => {
    console.log("Component mounted หรือ dependency เปลี่ยน");

    return () => {
      console.log("Cleanup - component unmount หรือก่อน effect ครั้งต่อไป");
    };
  }, [dependencies]);

  return <div>...</div>;
}
```

**useEffect มี 3 ลักษณะ:**

```javascript
// 1️⃣ ทำงานทุกครั้ง render (⚠️ ห้ามใช้ - อันตราย)
useEffect(() => {
  console.log("ทำงานทุกครั้ง");
});

// 2️⃣ ทำงาน 1 ครั้ง (เหมาะสม)
useEffect(() => {
  console.log("ทำงานแค่ครั้งแรก - perfect สำหรับ fetch data");
}, []);

// 3️. ทำงานเมื่อ dependency เปลี่ยน
useEffect(() => {
  console.log("userId เปลี่ยน ทำงาน");
}, [userId]);
```

### ขั้นตอน 1.2: ตัวอย่าง useEffect แรก

สร้างไฟล์ `src/App.jsx`:

```javascript
import { useState, useEffect } from "react";

export default function App() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState("");

  // 1. ทำงานครั้งแรกเท่านั้น
  useEffect(() => {
    console.log("Component mounted!");
  }, []);

  // 2. ทำงานทุกครั้งที่ count เปลี่ยน
  useEffect(() => {
    console.log(`📊 Count เปลี่ยนเป็น: ${count}`);
  }, [count]);

  // 3. ทำงานเมื่อ message เปลี่ยน
  useEffect(() => {
    const timer = setTimeout(() => {
      console.log(`💬 Message: ${message}`);
    }, 2000);

    // Cleanup function
    return () => clearTimeout(timer);
  }, [message]);

  return (
    <div style={{ padding: "20px" }}>
      <h1>🎯 useEffect Examples</h1>

      <div>
        <p>Count: {count}</p>
        <button onClick={() => setCount(count + 1)}>Increment</button>
      </div>

      <div style={{ marginTop: "20px" }}>
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="พิมพ์ข้อความ"
        />
        <p>Message: {message}</p>
      </div>

      <p style={{ color: "#999", fontSize: "12px" }}>
        ดูที่ Console (F12) เพื่อเห็น logs
      </p>
    </div>
  );
}
```

**ทดสอบ:** เปิด Console (F12) และสังเกตการทำงาน

---

## 📋 ส่วนที่ 2: Fetching Data from API

### ขั้นตอน 2.1: ดึงข้อมูลจาก Public API

```javascript
import { useState, useEffect } from "react";

export default function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUsers() {
      try {
        setLoading(true);
        const response = await fetch(
          "https://jsonplaceholder.typicode.com/users",
        );

        if (!response.ok) {
          throw new Error("Failed to fetch");
        }

        const data = await response.json();
        setUsers(data);
        setError(null);
      } catch (err) {
        setError(err.message);
        setUsers([]);
      } finally {
        setLoading(false);
      }
    }

    fetchUsers();
  }, []); // ⚠️ [] (dependency ว่าง) = ทำงาน 1 ครั้ง

  if (loading) return <p>⏳ Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      <h2>👥 Users ({users.length})</h2>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <strong>{user.name}</strong> - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}
```

### ขั้นตอน 2.2: Fetch เมื่อ Dependency เปลี่ยน

```javascript
import { useState, useEffect } from "react";

export default function PostsByUser() {
  const [userId, setUserId] = useState(1);
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    async function fetchPosts() {
      setLoading(true);
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/posts?userId=${userId}`,
      );
      const data = await response.json();
      setPosts(data);
      setLoading(false);
    }

    fetchPosts();
  }, [userId]); // ⚠️ userId เปลี่ยน ทำงานเลย

  return (
    <div style={{ padding: "20px" }}>
      <div>
        <label>
          Select User:
          <select value={userId} onChange={(e) => setUserId(e.target.value)}>
            {[1, 2, 3, 4, 5].map((id) => (
              <option key={id} value={id}>
                User {id}
              </option>
            ))}
          </select>
        </label>
      </div>

      {loading && <p>⏳ Loading posts...</p>}

      <h3>📝 Posts by User {userId}</h3>
      {posts.map((post) => (
        <div
          key={post.id}
          style={{
            border: "1px solid #ddd",
            padding: "10px",
            margin: "10px 0",
            borderRadius: "4px",
          }}
        >
          <h4>{post.title}</h4>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}
```

---

## 📋 ส่วนที่ 3: Cleanup Function

### ขั้นตอน 3.1: เพื่อป้องกัน Memory Leaks

```javascript
import { useState, useEffect } from "react";

export default function Timer() {
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  useEffect(() => {
    if (!isRunning) return; // ถ้าไม่ run ไม่ต้อง set timer

    const timer = setInterval(() => {
      setTime((prev) => prev + 1);
    }, 1000);

    // Cleanup: ปลดปล่อย timer เมื่อ component unmount หรือ isRunning เปลี่ยน
    return () => {
      console.log("🧹 Cleanup timer");
      clearInterval(timer);
    };
  }, [isRunning]);

  return (
    <div style={{ padding: "20px" }}>
      <h2>⏱️ Timer: {time}s</h2>
      <button onClick={() => setIsRunning(!isRunning)}>
        {isRunning ? "Stop" : "Start"}
      </button>
      <button onClick={() => setTime(0)}>Reset</button>
    </div>
  );
}
```

### ขั้นตอน 3.2: นำเอา Event Listener ออก

```javascript
import { useState, useEffect } from "react";

export default function WindowSize() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }

    // Add event listener
    window.addEventListener("resize", handleResize);

    // Cleanup: นำเอา event listener ออก
    return () => {
      console.log("🧹 นำเอา resize listener ออก");
      window.removeEventListener("resize", handleResize);
    };
  }, []); // ทำงาน 1 ครั้งเมื่อ mount แล้ว Cleanup ตอน unmount

  return (
    <div>
      <h2>📏 Window Width: {width}px</h2>
      <p>ลองเปลี่ยนขนาดหน้าต่างบราว์เซอร์ดู</p>
    </div>
  );
}
```

---

## 📋 ส่วนที่ 4: Advanced Patterns

### ขั้นตอน 4.1: Abort Controller (ป้องกัน Race Conditions)

```javascript
import { useState, useEffect } from "react";

export default function SearchUsers() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!query.trim()) {
      setResults([]);
      return;
    }

    // สร้าง AbortController เพื่อยกเลิก request ที่ค้างอยู่
    const controller = new AbortController();

    async function search() {
      setLoading(true);
      try {
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/users?q=${query}`,
          { signal: controller.signal },
        );
        const data = await response.json();
        setResults(data);
      } catch (err) {
        if (err.name !== "AbortError") {
          console.error(err);
        }
      } finally {
        setLoading(false);
      }
    }

    // Delay search เล็กน้อย (debounce)
    const timer = setTimeout(search, 500);

    // Cleanup
    return () => {
      clearTimeout(timer);
      controller.abort(); // ยกเลิก fetch ที่ยังค้าง
    };
  }, [query]);

  return (
    <div style={{ padding: "20px" }}>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="🔍 ค้นหา users..."
      />

      {loading && <p>⏳ Searching...</p>}

      <ul>
        {results.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}
```

### ขั้นตอน 4.2: Multiple Effects

```javascript
import { useState, useEffect } from "react";

export default function DataDashboard() {
  const [users, setUsers] = useState([]);
  const [posts, setPosts] = useState([]);
  const [comments, setComments] = useState([]);

  // Effect 1: Fetch users
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((r) => r.json())
      .then(setUsers);
  }, []);

  // Effect 2: Fetch posts
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((r) => r.json())
      .then(setPosts);
  }, []);

  // Effect 3: Fetch comments
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/comments")
      .then((r) => r.json())
      .then(setComments);
  }, []);

  return (
    <div style={{ padding: "20px" }}>
      <h1>📊 Dashboard</h1>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr" }}>
        <div style={{ border: "1px solid #ddd", padding: "10px" }}>
          <h3>👥 Users: {users.length}</h3>
        </div>
        <div style={{ border: "1px solid #ddd", padding: "10px" }}>
          <h3>📝 Posts: {posts.length}</h3>
        </div>
        <div style={{ border: "1px solid #ddd", padding: "10px" }}>
          <h3>💬 Comments: {comments.length}</h3>
        </div>
      </div>
    </div>
  );
}
```

---

## 📋 ส่วนที่ 5: Challenge

### Challenge 1: สร้าง Weather App

```javascript
import { useState, useEffect } from "react";

function WeatherApp() {
  const [city, setCity] = useState("Bangkok");
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchWeather() {
      setLoading(true);
      try {
        // ใช้ Open-Meteo API (ฟรี ไม่ต้อง API key)
        const response = await fetch(
          `https://geocoding-api.open-meteo.com/v1/search?name=${city}&count=1`,
        );
        const data = await response.json();

        if (data.results?.length > 0) {
          const { latitude, longitude } = data.results[0];

          const weatherResponse = await fetch(
            `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,weather_code`,
          );
          const weatherData = await weatherResponse.json();
          setWeather(weatherData.current);
          setError(null);
        } else {
          setError("City not found");
        }
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    fetchWeather();
  }, [city]);

  return (
    <div style={{ padding: "20px" }}>
      <h1>🌦️ Weather App</h1>

      <input
        type="text"
        value={city}
        onChange={(e) => setCity(e.target.value)}
        placeholder="Enter city..."
      />

      {loading && <p>⏳ Loading...</p>}
      {error && <p style={{ color: "red" }}>{error}</p>}

      {weather && (
        <div style={{ marginTop: "20px", fontSize: "24px" }}>
          <h2>📍 {city}</h2>
          <p>🌡️ Temperature: {weather.temperature_2m}°C</p>
          <p>🌤️ Weather Code: {weather.weather_code}</p>
        </div>
      )}
    </div>
  );
}

export default WeatherApp;
```

### Challenge 2: สร้าง Infinite Scroll

ให้สร้าง Component ที่โหลดข้อมูลเมื่อ scroll ถึงด้านล่าง:

```javascript
// ให้เขียน Component นี้เอง:
// 1. โหลด posts เริ่มแรก 10 รายการ
// 2. เมื่อ scroll ถึงด้านล่าง โหลด 10 รายการเพิ่มเติม
// 3. แสดง loading state เมื่อกำลังโหลด
// 4. จัดการ duplicate posts ด้วย Set หรือ filter
```

---

## 📝 ส่วนเพิ่มเติม

### การใช้งานที่ถูกต้อง

```javascript
// 1. ใช้ dependency array อย่างถูกต้อง
useEffect(() => {
  // code here
}, [userId]); // เมื่อ userId เปลี่ยนเท่านั้น

// 2. ใช้ cleanup function
useEffect(() => {
  const timer = setInterval(fn, 1000);
  return () => clearInterval(timer); // ลบ timer
}, []);

// 3. async function อยู่ในside effect
useEffect(() => {
  async function fetchData() {
    // fetch here
  }
  fetchData();
}, []);
```

### ผิด

```javascript
// 1. ลืม dependency array
useEffect(() => {
  fetchData(); // ทำงานทุกครั้ง render - อันตราย (ไม่ต้องใช้)
});

// 2. ลืม cleanup
useEffect(() => {
  const timer = setInterval(fn, 1000);
  // ลืม return cleanup - memory leak!
}, []);

// 3. ใส่ object/array ใน dependency
useEffect(() => {
  fetch(url);
}, [user]); // user object ใหม่ทุกครั้ง render!
```

---

## � Common Mistakes & Debugging

### ❌ Problem 1: Infinite Loop (Too Many Renders)

**สัญญาณ:**

- Console มี error: "Too many re-renders"
- Component render เรื่อย ๆ ไม่หยุด

**โค้ดผิด:**

```javascript
useEffect(() => {
  setCount(count + 1); // ← ทำให้ re-render → useEffect รันอีก
}); // ← ไม่มี dependency array
```

**วิธีตรวจสอบ:**

1. เปิด Console (F12)
2. ดู error message ว่ากล่าวถึง useEffect หรือ state
3. นับว่า log ซ้ำกี่ครั้ง

**วิธีแก้:**

```javascript
// เพิ่ม dependency array
useEffect(() => {
  setCount(count + 1);
}, []); // ← ตอนแรก count=0 → 1 แล้วหยุด

// หรือใช้อื่นแทน
const increment = () => setCount(count + 1);
// useEffect ไม่ต้อง call increment
```

---

### ❌ Problem 2: Memory Leak Warning

**สัญญาณ:**

- Console warning: "Can't perform a React state update on an unmounted component"
- Browser ช้าลง memory เพิ่มขึ้นเรื่อย

**โค้ดผิด:**

```javascript
useEffect(() => {
  const timer = setInterval(() => setCount((c) => c + 1), 1000);
  // ลืม cleanup ← timer ยังวิ่ง
}, []);
```

**วิธีตรวจสอบ:**

1. เปิด Console (F12) → network tab
2. ดูจำนวน API call - มีมากกว่า 1 ครั้งไหม?
3. DevTools Profiler → ดู Committed at time

**วิธีแก้:**

```javascript
useEffect(() => {
  const timer = setInterval(() => setCount((c) => c + 1), 1000);

  // เพิ่ม cleanup
  return () => {
    console.log("🧹 Cleanup timer");
    clearInterval(timer);
  };
}, []);
```

---

### ❌ Problem 3: Fetch API ทำงาน 2 ครั้ง

**สัญญาณ:**

- Network tab: API call ซ้ำ 2 ครั้ง
- Data load ช้า

**โค้ดผิด:**

```javascript
useEffect(() => {
  async function fetchData() {
    /* ... */
  }
  fetchData();
  fetchData(); // ← บังเอิญเรียก 2 ครั้ง
}, []);
```

**วิธีตรวจสอบ:**

1. เปิด DevTools → Network tab
2. Filter: "fetch" หรือ URL ที่ต้องการ
3. ดูเลขที่ request ว่ากี่ครั้ง (ควรเป็น 1)

**วิธีแก้:**

```javascript
useEffect(() => {
  let isMounted = true;

  async function fetchData() {
    const response = await fetch(url);
    const data = await response.json();

    if (isMounted) {
      // ✅ ตรวจสอบ
      setData(data);
    }
  }

  fetchData();

  return () => {
    isMounted = false; // ← ป้องกัน memory leak
  };
}, []);
```

หรือใช้ **AbortController:**

```javascript
useEffect(() => {
  const controller = new AbortController();

  async function fetchData() {
    const response = await fetch(url, { signal: controller.signal });
    const data = await response.json();
    setData(data);
  }

  fetchData();

  return () => controller.abort(); // ← ยกเลิก fetch
}, []);
```

---

### ✅ Debugging Tools & Techniques

#### 1️⃣ Console.log วิธีถูก

```javascript
useEffect(() => {
  console.log("🎯 useEffect ran"); // Mount/update

  return () => {
    console.log("🧹 Cleanup ran"); // Unmount/update
  };
}, [dependency]);
```

**วิธีเช็ค:**

- F12 → Console
- มี message ซ้ำกี่ครั้ง?
- Cleanup ทำงานไหม?

#### 2️⃣ DevTools React Profiler

```
DevTools → Profiler tab
↓
Click "Record" button
↓
Interact with your component (click, type, etc)
↓
ดู charts: ว่า useEffect ทำงานกี่ครั้ง
```

#### 3️⃣ Chrome Network Tab

```
DevTools → Network tab
↓
Reload page
↓
Filter by request type (fetch, xhr)
↓
ดูจำนวน request: ควรเท่ากับที่期望
```

---

### 📊 Debugging Checklist

```javascript
useEffect(() => {
  console.log("1️⃣ Start - dependency changed?", { dependency });

  // 2️⃣ Check if mounted
  let isMounted = true;

  async function doSomething() {
    console.log("2️⃣ Fetching...");
    // ... async work ...
    console.log("3️⃣ Got response");

    if (isMounted) {
      console.log("4️⃣ Setting state");
      // setState
    } else {
      console.log("4️⃣ Component unmounted, skipping setState");
    }
  }

  doSomething();

  // 5️⃣ Cleanup
  return () => {
    console.log("5️⃣ Cleanup - unmounting");
    isMounted = false;
  };
}, [dependency]);
```

---

## �📊 Common useEffect Patterns

| สถานการณ์                  | Dependency             | ประเภท      |
| -------------------------- | ---------------------- | ----------- |
| ทำงาน 1 ครั้ง (mounting)   | `[]`                   | Initialize  |
| ทำงานทุกครั้ง              | (ไม่ใส่)               | อันตราย     |
| ทำงานเมื่อตัวแปร X เปลี่ยน | `[x]`                  | Watch       |
| ทำงานแล้ว cleanup          | `[]` + return          | Lifecycle   |
| Conditional effect         | if statement ใน effect | Conditional |

---

## 🧪 การทดสอบ (Testing)

### ทดสอบ 1: ตรวจสอบ Console

- เปิด DevTools (F12)
- Console tab - ดูการทำงาน effect
- Network tab - ดูการ fetch API

### ทดสอบ 2: ทดสอบ Cleanup

```javascript
// สร้าง component ที่ mount/unmount
const [showComponent, setShowComponent] = useState(true);

{
  showComponent && <YourComponent />;
}
<button onClick={() => setShowComponent(!showComponent)}>Toggle</button>;
```

### ทดสอบ 3: React DevTools Profiler

- ติดตั้ง React DevTools extension
- Profiler tab - เห็นการ render ทั้งหมด

---

## Checklist

- [ ] เข้าใจ useEffect lifecycle
- [ ] สร้าง effect ที่ทำงาน 1 ครั้ง (mounting)
- [ ] ใช้ dependency array ได้ถูกต้อง
- [ ] เขียน cleanup function สำหรับ timers/listeners
- [ ] ดึงข้อมูลจาก API ได้
- [ ] จัดการ loading/error states
- [ ] ใช้ AbortController ป้องกัน race conditions
- [ ] สร้างอย่างน้อย 1 Challenge component

---

## 🎓 ขั้นตอนถัดไป

ในสัปดาห์ถัดไป เราจะเรียนรู้:

- Custom Hooks - สร้าง Hooks เป็นของเรา
- Context API - ส่งข้อมูลทั่ว Component tree
- Performance Optimization - useMemo, useCallback

---

## 📚 ทรัพยากรเพิ่มเติม

- [React Official: useEffect](https://react.dev/reference/react/useEffect)
- [JSONPlaceholder API](https://jsonplaceholder.typicode.com/)
- [Open-Meteo Weather API](https://open-meteo.com/)
- [MDN: AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)

---

## 📝 คำถามท้ายปฏิบัติการ - วัดความเข้าใจ

### ส่วนที่ 1: useEffect Basics

**คำถาม 1.1:** useEffect dependency array มี 3 ลักษณะ จงอธิบายแต่ละแบบ และบอก use case

A) ไม่ใส่ dependency array
B) ใส่ empty array `[]`
C) ใส่ตัวแปร `[var1, var2]`

---

**คำถาม 1.2:** Cleanup function ใน useEffect ใช้เพื่ออะไร? ยกตัวอย่าง 2 กรณี

---

**คำถาม 1.3:** ข้อไหนคือ infinite loop (คำตอบมีมากกว่า 1 ข้อ)

```javascript
// A)
useEffect(() => {
  setCount(count + 1);
}, []);

// B)
useEffect(() => {
  setCount(count + 1);
}, [count]);

// C)
useEffect(() => {
  setCount(count + 1);
});
```

---

### ส่วนที่ 2: API Fetching

**คำถาม 2.1:** จงเขียน Component ที่ fetch users จาก API จำนวน 10 คน พร้อม loading/error states

---

**คำถาม 2.2:** Race condition คืออะไร? และจะป้องกันได้ไม่ อธิบายวิธี 2 วิธี

---

**คำถาม 2.3:** หากมี input ที่ user พิมพ์และต้องการ search ทุกครั้ง มีปัญหาอะไร? วิธีแก้คือ?

---

### ส่วนที่ 3: Cleanup & Side Effects

**คำถาม 3.1:** สร้าง Component ที่มี timer ที่ต้องเริ่มต้นและหยุด โดยต้องมี cleanup function

---

**คำถาม 3.2:** Event listener และ setInterval ต้องใช้ cleanup หรือไม่? ทำไม?

---

### ส่วนที่ 4: Advanced Patterns

**คำถาม 4.1:** จงอธิบาย AbortController ใช้เพื่ออะไร? และทำการเขียนตัวอย่างการใช้

---

**คำถาม 4.2:** จงสร้าง Custom Hook ชื่อ `useFetch` ที่:

- รับ URL เป็น parameter
- return `{ data, loading, error }`
- จัดการ loading/error states
- ใช้ cleanup function

---
