← JavaScript Learning Path

JAVASCRIPT V2 · BROWSER · 68

fetch / API:Browser 怎麼把遠端資料接回 State?

fetch() 不是「拿 JSON 的魔法」。它啟動 HTTP request,得到 Response,你再檢查 status、讀 body、解析 representation、更新 JavaScript state,最後 render UI。這一課要把整條 request lifecycle 看清楚。

Learning outcomes

1. Why now:Local state 不等於真實系統資料

上一課的 students array 只活在 Browser memory。真正的 app 往往需要從 API 取得資料:

User clicks Load
  ↓
fetch("/api/students")
  ↓
HTTP request
  ↓
server
  ↓
HTTP response
  ↓
parse JSON
  ↓
students state
  ↓
render()

每一個箭頭都是獨立 failure boundary。

2. fetch() 先得到 Response,不是直接得到 JSON object

const response =
  await fetch("/api/students");

console.log(
  response.status,
  response.ok
);

const data =
  await response.json();

Response 描述 HTTP response;json() 才把 body representation 讀取並 parse 成 JavaScript value。

3. Worked Example A:最小 GET lifecycle

async function loadStudents() {
  const response =
    await fetch("/api/students");

  if (!response.ok) {
    throw new Error(
      "HTTP " + response.status
    );
  }

  const students =
    await response.json();

  return students;
}

這裡有至少三個不同 failure:fetch 本身 reject、status 非 2xx、body 不是合法 JSON。

4. HTTP 404 / 500 通常不會讓 fetch 自動 reject

const response =
  await fetch("/missing");

console.log(response.status); // 404
console.log(response.ok);     // false

DNS/TCP/TLS/connection 等 network-level failure 才常讓 Promise reject。HTTP 500 是「server 有回應,而且 status 表示 failure」。所以只寫 catch 而不檢查 status 是不完整的。

5. Worked Example B:先看 raw body,再決定 parse

const response =
  await fetch("/api/students");

const raw =
  await response.text();

console.log({
  status: response.status,
  contentType:
    response.headers.get(
      "content-type"
    ),
  raw
});

如果 server 實際回 HTML error page,硬做 response.json() 只會得到第二個錯誤,遮住原本的 server response。

6. POST JSON:request 也有 method / headers / body

const response =
  await fetch(
    "/api/students",
    {
      method: "POST",
      headers: {
        "Content-Type":
          "application/json"
      },
      body: JSON.stringify({
        name: "Amy",
        score: 90
      })
    }
  );

Client 能送 request,不代表 server 會接受。Validation、authentication、authorization 仍由可信 server/data layer enforce。

7. UI state 應該是一個可推理的狀態機

{
  status: "idle",
  data: [],
  error: null
}

{
  status: "loading",
  data: [],
  error: null
}

{
  status: "success",
  data: students,
  error: null
}

比起散落的 isLoading、hasError、done booleans,明確 state machine 更不容易出現互相矛盾的狀態。

8. Worked Example C:Fetch → State → Render

async function refresh() {
  state = {
    status: "loading",
    data: [],
    error: null
  };
  render();

  try {
    const data =
      await loadStudents();

    state = {
      status: "success",
      data,
      error: null
    };
  } catch (error) {
    state = {
      status: "error",
      data: [],
      error: error.message
    };
  }

  render();
}

這裡真正重要的是每個 transition 都能被觀察與測試。

9. Response body 只能被 consume:設計 debugging 時要注意

const response =
  await fetch(url);

const data =
  await response.json();

// 再次讀同一 body
// 可能失敗,因為 stream 已讀取

初學階段不必深入 Streams API,但要知道 response body 不是永遠可以反覆 parse 的普通 object。

10. Stale response:後送的 request 不一定後回

search("java"); // request A
search("js");   // request B

// B 先回
// A 後回
// 如果無保護,舊 A 可能覆蓋新 B

這不是「網路很怪」,而是 concurrency ordering 問題。

11. Cancellation:AbortController

let controller;

async function search(query) {
  controller?.abort();
  controller =
    new AbortController();

  const response =
    await fetch(
      "/api/search?q=" +
        encodeURIComponent(query),
      {
        signal:
          controller.signal
      }
    );

  return response.json();
}

132 會更深入 race / latest-wins;這裡先知道舊 request 可以被取消。

12. Common mistakes

A. 以為 500 一定進 catch

先檢查 response.ok/status。

B. 把 Response 當成 parsed JSON

還要讀 body。

C. Loading 永遠不結束

Error path 忘了更新 state。

D. 每次 input 都發 request,舊結果覆蓋新結果

需要 debounce/cancellation/version strategy。

13. Debug evidence:不要只看 UI 顯示「API failed」

console.log({
  url: response.url,
  status: response.status,
  ok: response.ok,
  contentType:
    response.headers.get(
      "content-type"
    )
});
  1. Network request 有沒有真的送出?
  2. Final URL / method / status 是什麼?
  3. Response body 寫什麼?
  4. Body representation 與 Content-Type 一致嗎?
  5. Parse 成功後 state 是否更新?
  6. 最後 render 是否執行?

14. Guided exercise:完成 loadTitle(url)

async function loadTitle(url) {
  const response =
    await fetch(url);

  // 1. parse JSON
  // 2. return data.title
}

先說明 response 與 data 的 type/role,再完成本頁 executable mastery tests。

15. Independent exercise:寫一個有 HTTP check 的 loader

完成 loadCourses(url):fetch → 若 !response.ok throw HTTP n → parse JSON → 必須確認結果是 array,否則 throw Invalid payload。

16. Challenge:設計 Search latest-wins

使用者快速輸入 java、javascript、js。設計一個方案保證最後 UI 不會被較舊 request 覆蓋。可以選 AbortController 或 request version;要寫出 state transitions。

Project checkpoint:Grade Tracker Remote Data

Load button
  ↓ event
refresh()
  ↓
state = loading
  ↓
fetch API
  ↓
status + body
  ↓
students[]
  ↓
state = success/error
  ↓
render()

現在 Grade Tracker 從純 Browser memory 進入真正 client/server boundary。下一課要學會用 DevTools 看這條路徑每一層的 evidence。

17. Vocabulary / Summary

18. Further reading

Knowledge check

  1. 解釋 fetch resolve 與 HTTP success 為何不是同一件事。
  2. 區分 Response object 與 parsed JSON value。
  3. 畫出 loading → success/error state transitions。
  4. 說明 stale response 如何產生。
  5. 完成本頁 Fetch + JSON executable mastery tests。