上一篇提到 useEffect 時,我一直在問:
這段程式到底是在什麼時候做事情?
到了實際 React 專案,又很常看到另一種寫法:
const { loading, run } = useRequest(
async () => {
...
},
{
manual: true,
onSuccess: result => {
...
},
}
);
以前看到這種程式碼,我的第一個反應通常是:
useRequest 是什麼?
loading 是什麼?
run 又是什麼?
manual 又是什麼?
onSuccess 又跑去哪?
結果每一個都查完之後,還是不知道:
這支 API 到底什麼時候被打出去?
後來我才慢慢改成另一種讀法。
先不要把每一個參數都研究完。
先追:
誰觸發 Request?
↓
Request 打哪支 API?
↓
Response 回來之後去哪裡?
↓
最後影響什麼畫面?
例如:
const { loading: loadingInvoice, run: loadInvoice } =
useRequest(
async () => {
const id = resolveInvoiceId(location.search);
if (id) {
return InvoiceService.getById(id);
}
throw Error("No Invoice Id");
},
{
manual: true,
onSuccess: result => {
setInvoice(result);
form.setFieldsValue({
discount: result.discount,
});
},
onError: error => {
notification.error({
message: error.message,
});
},
}
);
第一次看到這整段,真的很容易直接放棄。
所以還是先不要逐行讀。
先找最重要的四個位置:
① Request 怎麼被觸發?
② API 在哪裡?
③ Response 是誰?
④ Response 最後去了哪?
我通常會先掃這一段:
return InvoiceService.getById(id);
看到:
Service.getById(...)
就可以先猜到:
這裡很可能是在取得某一筆資料。
所以先畫:
InvoiceService.getById(id)
↓
發出 Request
↓
取得 Invoice 資料
至於:
InvoiceService
裡面真正的 URL 是什麼、用的是 axios 還是其他 HTTP Client,
可以等真的需要時再往下追。
第一輪先知道:
API 從這裡出去。
就夠了。
id 又是哪裡來的?往上一點:
const id = resolveInvoiceId(location.search);
所以:
location.search
↓
resolveInvoiceId()
↓
id
如果網址可能是:
...?invoiceId=123
那這裡的目的就是從目前 URL 資訊中取得需要的 id。
接著:
if (id) {
return InvoiceService.getById(id);
}
就變成:
網址
↓
取出 id
↓
有 id
↓
getById(id)
↓
呼叫 API
原本一大段程式突然就剩一條線。
return 回來的東西去哪?接下來這個問題很重要。
return InvoiceService.getById(id);
API 成功後取得的結果,會進到:
onSuccess: result => {
...
}
所以可以先畫:
InvoiceService.getById(id)
↓
API Response
↓
result
↓
onSuccess
這時候:
result
就是我們從 Request 成功結果裡拿到的資料。
看 onSuccess:
onSuccess: result => {
setInvoice(result);
form.setFieldsValue({
discount: result.discount,
});
}
先拆第一個:
setInvoice(result);
可以理解成:
API Response
↓
result
↓
setInvoice()
↓
存進 React State
所以:
invoice
之後可能會被畫面其他地方使用。
第二個:
form.setFieldsValue({
discount: result.discount,
});
這裡不是把整份 Response 放進 Form。
而是從:
result
裡面拿:
result.discount
再設定到表單的:
discount
欄位。
所以:
API Response
↓
result.discount
↓
form.setFieldsValue()
↓
Form 的 discount 欄位
到這裡其實已經能畫出完整資料流:
URL
↓
resolveInvoiceId()
↓
id
↓
InvoiceService.getById(id)
↓
API
↓
Response
↓
result
├─→ setInvoice(result)
│ ↓
│ State
│
└─→ result.discount
↓
setFieldsValue()
↓
Form
這張圖比背 useRequest 語法重要很多。
run 是什麼?前面有:
const {
loading: loadingInvoice,
run: loadInvoice,
} = useRequest(...);
這裡先看:
run: loadInvoice
意思是把 useRequest 提供的:
run
重新命名成:
loadInvoice
所以程式其他地方可能會看到:
loadInvoice();
這時候我現在就知道:
這很可能是在手動觸發剛才那個 Request。
也就是:
loadInvoice()
↓
執行 useRequest 裡的 async function
↓
取得 id
↓
getById(id)
↓
API Request
manual: true?這個例子裡還有:
manual: true
在這類 useRequest 寫法中,可以先把它理解成:
不要建立後就立刻自動執行,我要自己決定什麼時候
run()。
所以:
manual: true
搭配:
loadInvoice();
資料流會比較像:
建立 useRequest
↓
先不打 API
某個地方呼叫
loadInvoice()
↓
Request 才開始
這就跟上一篇 useEffect(() => fetchData(), []) 的概念有點不同。
上一篇:
Component Render
↓
Effect
↓
Request
這裡可能是:
某個事件 / 某段流程
↓
loadInvoice()
↓
Request
所以看到 useRequest 時,我現在會特別去找:
loadInvoice()
到底出現在哪裡。
因為:
找到
run(),通常就找到「誰真正觸發這支 API」的重要線索。
loading 又是哪裡來的?這段:
loading: loadingInvoice
同樣是在重新命名。
可以先理解成:
useRequest 提供 loading
↓
重新命名
↓
loadingInvoice
它通常用來表示:
Request 還在進行中嗎?
畫面可能會用:
loading={loadingInvoice}
或:
<Button loading={loadingInvoice}>
Submit
</Button>
所以:
Request 開始
↓
loading = true
Request 結束
↓
loading = false
先理解用途即可,不用一開始研究它背後怎麼實作。
這個例子還有:
onError: error => {
notification.error({
message: error.message,
});
}
所以整體不只有成功路線。
還有:
Request
↓
成功?
├─ Yes
│ ↓
│ onSuccess
│ ↓
│ 更新 State / Form
│
└─ No
↓
onError
↓
顯示錯誤訊息
這也是讀 Request 時很好用的一個方式:
不要只追 Happy Path,也找一下錯誤去哪裡。
useRequest,不會從第一行一路硬啃以前:
useRequest 是什麼?
↓
async 是什麼?
↓
manual 是什麼?
↓
formatResult 是什麼?
↓
onSuccess 是什麼?
↓
onError 是什麼?
查完一輪,知識增加了,理解沒有增加。
現在我比較會先找:
① 誰觸發?
→ run / loadInvoice
② 打哪支 API?
→ InvoiceService.getById(id)
③ API 需要什麼?
→ id
④ Response 回來叫什麼?
→ result
⑤ result 去哪?
→ setInvoice()
→ form.setFieldsValue()
⑥ 失敗去哪?
→ onError
先把主幹畫出來:
loadInvoice()
↓
取得 id
↓
Service.getById(id)
↓
Response
↓
onSuccess
├─ State
└─ Form
再回頭補不熟的語法。
如果今天問題變成:
「到底實際打了哪個 API?」
那才繼續進:
InvoiceService.getById(id)
找到 Service 裡真正的 Request。
可能會長得類似:
getById(id) {
return request(`/invoices/${id}`);
}
這時資料流再往下多一層:
Component
↓
useRequest
↓
InvoiceService.getById(id)
↓
HTTP Request
↓
Backend API
所以我現在不會看到 Service 就停住。
而是看目前 Debug 的問題需要追到哪一層。
useRequest API 大全不同專案使用的 Request 工具、設定方式可能不同。
今天真正想記的是:
Trigger
↓
Request Function
↓
Service
↓
API
↓
Response
↓
Success / Error
↓
State / Form / UI
所以以後看到:
useRequest(...)
我會先問:
誰讓它開始跑?
接著:
它打去哪裡?
最後:
Response 回來之後,被放去哪裡?
只要這三個問題先找到,
原本看起來很大的 useRequest,就會開始變成一條可以追的資料流。
下一篇可以繼續沿著這條線往 Backend 前進:
Service.getById() 裡到底藏了什麼?一支 API 到底要怎麼從前端一路追到後端?