2025/06/13

【所謂的全端工程師,就是在面對突如其來的需求下,仍能給出合宜的解方】 ——打破程式鄙視鏈的迷思

作者:吳祐賓 

 



 

 


▋選擇 Classic ASP 來解決問題,不是瘋狂的叛逆,而是妥協的藝術



當面對一個微小需求:「使用者要在系統看到圖片」

而你那 2000 年開發的 Win32 系統既改不動,也不能改

一般人會有幾個選項:



1.說做不到,放棄

2.另外寫一個系統

 

選 1 的人很正常,領多少錢做多少事,老闆的薪水沒付到這個專業項目

選 2 的人超級認真,是一名做實事的員工,只是對應到「微小需求」的開發時間,聰明的老闆怎麼想都不認為是好的選項

身為一名專業的工程師,當然能不做就不做...我是說做不到的工作就給外包做



我選 3.外部連結或下載 來解決此問題,原因是:

1.繞過歷史包袱:避免於舊技術搏鬥、修改系統所引發的系統性風險 (好像繞口令)

2.成本效益最佳化:「使用者要在系統看到圖片」這句話的重點不是「在系統」,而是「看到圖片」,使用 OS 層的瀏覽器來開圖是成本最低、開發時間最短的方案




▋「成本最低、開發時間最短」的方案,你怎麼選?


 

利用瀏覽器開,可使用本機檔案開啟或另寫網頁圖片顯示功能

由於「本機檔案開啟」需要修改程式,不被許可,於是只能採用網頁圖片顯示

看到這裡,聰明的你應該會想到:



1.Python

2.Node.js

3.Deno

4.PHP

 

這些選項除了 4 之外,其它都是現代網頁開發的主流 (是,JSP 已成古代語了)

Python 絕對是開發時間最短的選項,豐富的三方資源,簡單直白的程式寫碼風格,都是上上之選

然而,除了開發環境複雜,開發除錯服務緩慢,網站服務還得設定反向代理(Reverse proxy),嗯,還是輕輕放下

Node.js / Deno 這兩個都是我目前的主要工具,豐富的 JS、三方資源,但為了實現小功能而承擔 node_modules 無底黑洞和使用反向代理等步驟,是我不考慮的原因

PHP,持續更新、活躍於現今的網頁開發市場,採 FastCGI,使 PHP 在穩定、執行效率更上一層樓,還是 NAS 的基本配備

但我的環境是 IIS + MSSQL 啊!PHP 的跨 NAS 機制對我來說吸引力不高,而 PHP 在 NAS 下要連 MSSQL 也是困難重重,點到 PHP 的技能樹對我的加分並不多




▋Classic ASP 位於程式鄙視鏈最底端,是它無用還是後門太多?


 
Classic ASP,目前版本 3.0(2000年2月17日,​距今至少 25 年前)

它還活著,Windows Server 2025 版,還是有內建 Classic asp 3.0

看到微軟廢棄了這麼多產品,IE6 都走入歷史了,Classic asp 居然還活著,證明 Classic asp 才是真正的歷史包袱

早些年,我也是臨危受命來解救公司內 asp 網站被殖入木馬的問題,所以對攻擊手法也是略懂略懂,不外乎就是被當 SQL 和 HTML 的測試場:

1. SQL 注入 (SQL injection)

2. XSS 注入 (Cross-Site Scripting,又稱跨站腳本攻擊)


綜合這些經驗,另外還有一個冷知識,asp 支援 JS ECMAScript 標準!就是 ECMA 3 (2000年標準) 舊了點

但它還是 JavaScript,在 JS 經驗上也能延續,哪怕 asp 是程式鄙視鍵的最底層,都不影響我使用 Classic asp 的決定




▋所謂的全端工程師,就是能在面對突如其來的需求下,仍能給出合宜的解方


 

透過這些分析可以知道,一名全端工程師,掌握一門可以前後端通透的技術外,巧妙的「技術選型」也是全端工程師必點的技能

使用 Classic asp,只是在我的場合下,能採用的最佳解,它可能不適合你,但上述的思路與大局觀,在任何場合都能適用

成為全端工程師,就是要持續的青春期叛逆


2025/05/09

還在 React 從入門到放棄?告訴你選擇比努力還重要的事

 作者:吳祐賓

 

 


 

 

用 Vite,別再用 CRA

 

2025年02月,React 官方公告,建議使用 Vite,所以別再用 CRA (create-react-app)

看了許多 React 的入門資料,還有很多古典資料,大多還是從 create-react-app 專案起始,用久了覺得很不舒服

 

NPM 編譯經常無預期出錯 


NPM 編譯經常無預期出錯,學習 React 免不了會經過一連串試錯的過程

但往往會不經意的出現許多編譯錯誤,例如在安裝某 UI 套件時,就遇到依賴套件版本衝突的問題,錯誤訊息洋洋灑灑一大篇,

指向的卻是 Webpack 內部模組的錯誤,對於當時的我來說,根本無從下手

在無法排除錯誤的情形下,最常做的就是重建 node_modules 內容,次數一多

是在學習 React 還是背誦 NPM i 指令,我已經搞不清楚了

 

 

在設計時期,前端和後端 API 網域位置不一致時

 

只要不是靜態網頁,在前端的開發過程一定會和後端 API 傳遞訊息,尤其是開發網頁應用程式更是必須

在設計時期,只要前端和後端的 Web 服務網域不同,就一定會遇上【跨來源資源共用(Cross-Origin Resource Sharing (CORS))】這個大魔王

後端就必須進行 CORS 設定,但實際上線又會是在同一網域,只有開發時期才會遇到的 CORS 顯得是非常詭異的歷程。

 

 

你以為要從 JavaScript ES6 開始學習,事實上

 

包含 React 官方網站,很多學習 React 的資料顯示,JavaScript ES6 是必須先學會的基礎,我認為這是造成 React 陡峭的學習曲線的罪魁禍首

許多社團新手遇到的問題大多是編譯 React 時產生的異常狀態,NPM 會回傳非常完整的【長篇報告】,也因為內容非常的多

若直接複製貼上,只會造成有心要幫忙的人在理解前還必須先把整篇報告看過一輪,除錯到心累。 



React 三大核心


React 是水很深的函式庫之一,技術中心思想在【狀態的傳遞】,基於中心所延伸出來的有三大核心:

 

  • React : 屬性、狀態、事件、元件和元件狀態互動等。
  • Router : 頁面到頁面間狀態的互動
  • Redux : 全域狀態管理

 

React 的問題大多就是狀態問題,Router 和 Redux 都是 React 官方沒做但是推薦的解決方案,都是學習 React 過程中必須學習的技能。

我歸納學習 React 學習資源後,發現從 create-react-app 幾乎就是給新人從入門到放棄的好專案,而我認為學習 React 路線有二:

  • 拿掉 NPM 編譯,純粹的 React
  • 擁抱編譯,從 Vite 開始


使用 esm.sh。拿掉 NPM 編譯,純粹的 React

 

 使用 esm.sh,就可以先專注在 React 的學習,esm.sh 也支援 JSX,範例如下:

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!doctype html>
<html>
<head>
<script type="importmap">
{
  "imports": {
    "react": "https://esm.sh/react@19.1.0",
    "react-dom/client": "https://esm.sh/react-dom@19.1.0/client"   
  }
}
</script>
<script type="module" src="https://esm.sh/tsx"></script>
</head>
<body>
  <div id="root"></div>
<script type="text/babel">
import React from "react";
import { createRoot } from 'react-dom/client';

const rootElement = document.getElementById("root");

const App = <h1>Hello, Eden</h1>;

const root = createRoot(rootElement);
root.render(App);

</script>
</body>
</html>

 

 <script type="importmap"> ... </script>

後面程式碼要使用 React 的時候,瀏覽器就知道要去 esm.sh 下載

 

 <script type="module" src="https://esm.sh/tsx"></script>

 esm.sh 讓瀏覽器看懂 JSX 語法

 

 

擁抱編譯,從 Vite 開始

 



在 npm 下,使用 vite cli 指令建立 vite react 專案


npm create vite@latest


接著按精靈指引,依序填入專案名稱、目錄名稱等內容


完成後,進入指定目錄就可以使用 npm 啟動 vite 建立的 React 網站,並開始學習 React 入門

 

  cd Eden-vite-project
  npm install
  npm run dev





2025/03/28

回歸網頁設計的原點 - HTMX

作者:吳祐賓













HTMX 和 VUE 一樣,是由個人開發的一款開源網頁函式庫 (Library,但 VUE 屬於 Framework,特此說明),前身名為 intercooler.js,由 Carson Gross 於 2013 年打造出的產品,在 2020 年改為 HTMX 後開始有知名度


▋HTMX 框架的核心目標



HTMX 核心目標,是為了讓 AJAX 變得簡單,丟掉 <a>、<form>


使用 html 標籤就能完成 AJAX 瑣碎的 JS 程式碼工作


2020 年在 HTMX 成立之初,就有多數開發者認為比 React、VUE、Angular 還要成熟的框架


由於 HTMX 和 React 同為 Library,所以,使用 React + HTMX 關鍵字查詢時,你會優先得到 HTMX is React killer 的比較資訊


更多的是教你如何從 React 移植到 HTMX 上


▋HTMX 實際設計



實際體驗相當簡單,不需要 Web Server,在單機就能體驗


▍首先,建立一個 html 檔案



▍在 html 引入 HTMX



雖然不用寫 js,但還是需要引入 HTMX 套件,另外,範例中會呼叫外部 API,所以要設定 "selRequestsOnly" 為 "false"


<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>

<meta name="htmx-config" content='{"selfRequestsOnly": false}' />


▍建立 div,設定 HTMX 呼叫 API



沒有 API 怎麼辦?


你可以參考我的另一篇 "My JSON Server -- 偽線上 REST 服務",把 API 連結套入進來


在 body 標籤裡寫下


<div hx-get="https://my-json-server.typicode.com/Eden5Wu/react-store-api/products">Download Products</div>



你應該就可以看到如圖的成果展示

 




▋總結:HTMX 將 ajax 完美的整合到 html 裡



從上面的範例可以看到 HTMX 把 ajax 變不見,只有單純的 html,使用 HTMX 才能算是真正意義上的把 "寫網頁" 這件事變簡單


由於只用上 html,所以很適合以模板引擎為基礎的後端服務框架 (SSR) 來使用,能夠提高使用者提驗及網站 SEO,這給後端工程師加速寫出網頁應用程式的方法,像 PHP、Python 等都很合適


在 HTMX 官網中的 "Server-Side Examples" 一節,提供了一些使用模板引擎的後端服務框架,讓後端工程師可以參考,這裡列出幾個常見的後端服務框架:


▸JavaScript Node.js

▸C# ASP.NET Core

▸C# Blazor

▸PHP Laravel

▸Python Django

▸Python FastAPI

▸Delphi WebBroker

▸FreePascal with Pas2JS

2025/03/14

程式碼轉換不再難:AI 助你精通 FastReport FastScript

 作者:吳祐賓

 

 


 

 

 

FastScript 是 FastReport VCL / FMX 超棒的產品之一,FastScript 功能非常豐富,七成需求都可以依賴它來完成,不一定要使用肥大的開發工具。

 

FastScript 限制

 

FastScript 也有其限制,例如它不支援以下兩種東西:

  • Class 建立。無法使用 record, class 等 object 來建立自己的物件
  • Set 集合。無法使用 [fsBold, ...]來設定集合

 

使用 AI,在 FastScript 上寫出一手好程式


還好現在有 AI 工具,把提示語給 AI 後,就可以快速寫出正確又有效率的工具出來,真是太棒了!

 

我使用提示語如下,提供給各位讀者參考:


使用 FastReport FastScript 的 JScript 語法建立一個腳本:[
  將 "Hello, World!" 顯示在 Memo1 元件中。(可以代換為你的需求)
]
Refer to the following FastScript JScript syntax rules:[
===
Classes
You cannot define a class inside the script, but you can use the external classes defined in add-in modules or your application.
===
Functions
There is a rich set of standard functions which can be used in a
script. To get an access to these functions, pass the fsGlobalUnit
reference to the TfsScript.Parent property.
===
About FastReport's FastScript:JScript syntax:
Program -> Statements
Statements -> Statement...
Block -> '{' Statements '}'
ImportStmt -> IMPORT (String/,)...
VarStmt -> VAR (VarDecl/',')...
VarDecl -> Ident [Array] [InitValue]Array -> '[' (ArrayDim/',')... ']'
ArrayDim -> Expression
InitValue -> '=' Expression
Expression -> SimpleExpression [RelOp SimpleExpression]...
SimpleExpression -> ['-'] Term [AddOp Term]...
Term -> Factor [MulOp Factor]...
Factor -> Designator
-> UnsignedNumber
-> String
-> '(' Expression ')'
-> '!' Factor
-> '[' SetConstructor ']'
-> NewOperator
-> '<' FRString '>'
SetConstructor -> SetNode/','...
SetNode -> Expression ['..' Expression]
NewOperator -> NEW Designator
RelOp -> '>'
-> '<'
-> '<='
-> '>='
-> '!='
-> '=='
-> IN
-> IS
AddOp -> '+'
-> '-'
-> '||'
-> '^'
MulOp -> '*'
-> '/'
-> '%'
-> '&&'
-> '<<'
-> '>>'
Designator -> ['&'] Ident ['.' Ident | '[' ExprList ']' | '(' [ExprList] ')']...
ExprList -> Expression/','...
Statement -> (AssignStmt | CallStmt | BreakStmt | ContinueStmt |
DeleteStmt | DoWhileStmt | ForStmt | FunctionStmt |
IfStmt | ImportStmt | ReturnStmt | SwitchStmt |
VarStmt | WhileStmt | WithStmt | Block) [';']
BreakStmt -> BREAK
ContinueStmt -> CONTINUE
DeleteStmt -> DELETE Designator
AssignStmt -> Designator ['+'|'-'|'*'|'/']'=' Expression
CallStmt -> Designator ['+''+'|'-''-']
ReturnStmt -> RETURN [Expression]
IfStmt -> IF '(' Expression ')' Statement [ELSE Statement]
SwitchStmt -> SWITCH '(' Expression ')' '{' (CaseSelector)... [DEFAULT ':' Statement] '}'
CaseSelector -> CASE SetConstructor ':' Statement
DoWhileStmt -> DO Statement [';'] WHILE '(' Expression ')' ';'
WhileStmt -> WHILE '(' Expression ')' Statement
ForStmt -> FOR '(' ForStmtItem ';' Expression ';' ForStmtItem ')' Statement
ForStmtItem -> AssignStmt-> CallStmt-> VarStmt-> Empty
TryStmt -> TRY CompoundStmt (FINALLY | EXCEPT) CompoundStmt
FunctionStmt -> FunctionHeading Block
FunctionHeading -> FUNCTION Ident FormalParameters
FormalParameters -> '(' [FormalParam/','...] ')'
FormalParam -> ['&'] Ident
WithStmt -> WITH '(' Designator ')' Statement
]
Specific usage of FastScript JScript: [
#language JScript // this is optional
import "unit1.js", "unit2.js"
Report.Memo1.Text = "HelloWorld"; // Sets the text of Memo1 through the Report object.
Memo1.Text = "HelloWorld";// Equivalent if Memo1 is in the main report scope (not within a sub-component like a Group or Band).
var mbSet = mbYes + mbNo;  // FastScript does not support sets directly; use addition for combining flags.
if (MessageDlg("Welcome to my JScript application. Exit now?", mtConfirmation, mbSet, 0) == mrYes)
{ ShowMessage("OK");}
// import section - must be before any other sections
var i, j = 0; // var section
function p1() // procedures and function
{//
}
// main procedure that will be executed.
p1();
// Dynamic array create. (var myarray = [] not support. Need use the FR TfrxArray Component)
var myarray = new TfrxArray();
myarray[0] = 1;
myarray[1] = 2;
for (i = 0; i < 1; i++) j = i + 1; myarray[i] = myarray[i] + j;
ShowMessage(IntToStr(myarray.Count)); // TfrxArray is not JScript array type.
// You can use the TStrings/TStringList. Same the Delphi TStrings/TStringList.
var myList = new TStringList();
myList.Add("message=HelloWorld");
ShowMessage(myList.Values("message"));
]
The following FastScript built-in functions are available:
function IntToStr(i: Integer): Stringfunction FloatToStr(e: Extended): Stringfunction DateToStr(e: Extended): Stringfunction TimeToStr(e: Extended): Stringfunction DateTimeToStr(e: Extended): Stringfunction VarToStr(v: Variant): Stringfunction StrToInt(s: String): Integerfunction StrToFloat(s: String): Extendedfunction StrToDate(s: String): Extendedfunction StrToTime(s: String): Extendedfunction StrToDateTime(s: String): Extendedfunction Format(Fmt: String; Args: array): Stringfunction FormatFloat(Fmt: String; Value: Extended): Stringfunction FormatDateTime(Fmt: String; DateTime: TDateTime): Stringfunction FormatMaskText(EditMask: string; Value: string): stringfunction EncodeDate(Year, Month, Day: Word): TDateTimeprocedure DecodeDate(Date: TDateTime; var Year, Month, Day: Word)function EncodeTime(Hour, Min, Sec, MSec: Word): TDateTimeprocedure DecodeTime(Time: TDateTime; var Hour, Min, Sec, MSec: Word)function Date: TDateTimefunction Time: TDateTimefunction Now: TDateTimefunction DayOfWeek(aDate: DateTime): Integerfunction IsLeapYear(Year: Word): Booleanfunction DaysInMonth(nYear, nMonth: Integer): Integerfunction Length(s: String): Integerfunction Copy(s: String; from, count: Integer): Stringfunction Pos(substr, s: String): Integerprocedure Delete(var s: String; from, count: Integer): Stringprocedure Insert(s: String; var s2: String; pos: Integer): Stringfunction Uppercase(s: String): Stringfunction Lowercase(s: String): Stringfunction Trim(s: String): Stringfunction NameCase(s: String): Stringfunction CompareText(s, s1: String): Integerfunction Chr(i: Integer): Charfunction Ord(ch: Char): Integerprocedure SetLength(var S: String; L: Integer)function Round(e: Extended): Integerfunction Trunc(e: Extended): Integerfunction Int(e: Extended): Integerfunction Frac(X: Extended): Extendedfunction Sqrt(e: Extended): Extendedfunction Abs(e: Extended): Extendedfunction Sin(e: Extended): Extendedfunction Cos(e: Extended): Extendedfunction ArcTan(X: Extended): Extendedfunction Tan(X: Extended): Extendedfunction Exp(X: Extended): Extendedfunction Ln(X: Extended): Extendedfunction Pi: Extendedprocedure Inc(var i: Integer; incr: Integer = 1)procedure Dec(var i: Integer; decr: Integer = 1)procedure RaiseException(Param: String)procedure ShowMessage(Msg: Variant)procedure Randomizefunction Random: Extendedfunction ValidInt(cInt: String): Booleanfunction ValidFloat(cFlt: String): Booleanfunction ValidDate(cDate: String): Booleanfunction CreateOleObject(ClassName: String): Variantfunction VarArrayCreate(Bounds: Array; Typ: Integer): Variant


前面的提示詞也可以使用:

 

將以下 C# 程式碼片段轉換為使用 JScript 語法的 FastReport FastScript。 該腳本應在 FastReport 環境中實現等效的功能。 請密切注意所提供的 FastScript JScript 語法規則和可用的內建函數。


總結

有了AI後,程式轉換上變得十分容易,從以前要求 Google 關鍵字力,現在要漸漸改為 AI 提示詞力。

 

如果有什麼想回饋的內容,歡迎在底下留言讓我知道。

2025/03/07

C++ Builder記憶體管理:使用unique_ptr實現物件自動釋放

作者:吳祐賓

 

 

據說,未來是個沒有 Delete 的世界(大誤)



 

C++ Builder開發者:硬核技術與直覺UI的雙重追求

 

在說明物件釋放方法之前,還得先聊聊 C++ Builder 開發者的習慣

 

就目前所接觸到會使用 C++ Builder 的開發者所整理的經驗,他們使用 C++ Builder 的理由大致上是以下兩點:


  1. 本身具有極高的 C 語言造詣,通常具有硬體開發經驗
  2. 和 Visual Studio C++ 相比,C++ Builder 的 UI 更是直覺的建立


 

由 1 可知,會使用 C 語言的開發者,通常有很強烈的語言潔癖,以及有自己一套對記憶體控制的要求

 

由 2 可知,C++ Builder 對從 C 過來的開發者來說,是很棒的 UI 建模工具



C++ Builder (CB) 中的物件釋放:常見問題與解決方案探討

 
只是,由於 CB 的 WinForm 是建構在 VCL framework 之上,所以學習 C++ 的物件自然是必須要的。在擴充C++的基本知識後,底下便是常見的寫法:

2025/03/06

node.js 的字串拼接函式比較

 作者:吳祐賓

 

 

 


 

 

最近在寫 Express.js API 時,被 AI 提示說直接字串拼接較不安全,建議使用指令處理。

 

但採用後原來的程式反而無法執行。檢查後才發現原來組合出來的路徑和我想的不一樣。

 

於是就做了一點測試比較:

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// node.js 的字串拼接函式比較:

// 1. 字串直接拼接
console.log('/A/B/C' + '/fetch_emp');
// 輸出 (所有系統, 但 Windows 上可能不正確): /A/B/C/fetch_emp
// 解釋:  直接使用 + 運算子。在 *nix (Linux/macOS) 上沒問題,但在 Windows 上可能因路徑分隔符 (\) 而出錯。不推薦。

// 2. path.join() - 跨平台安全 (推薦!)
console.log(path.join('/A/B/C', '/fetch_emp'));
// 輸出 (Windows): \A\B\C\fetch_emp
// 輸出 (Linux/macOS): /A/B/C/fetch_emp  (與上面相同,但更可靠)
// 解釋:  path.join() 根據 *當前作業系統* 自動選擇正確的分隔符。這是最安全、可靠的方法,確保跨平台兼容。

// 3. path.posix.join() - 單純傳入virtualDirPath
console.log(path.posix.join('/A/B/C'));
// 輸出 (所有系統): /A/B/C
// 解釋: 只傳入一個參數時,作用等同於 path.normalize(),用來規範化路徑。

// 4. path.posix.join() + ./  (當前目錄)
console.log(path.posix.join('/A/B/C', './'));
// 輸出 (所有系統): /A/B/C/
// 解釋:  path.posix.join() 強制使用 POSIX 分隔符 (/)。'./' 代表當前目錄,被絕對路徑 /A/B/C 吸收,最後加上/。

// 5. path.posix.join() + / (根目錄)
console.log(path.posix.join('/A/B/C', '/'));
// 輸出 (所有系統): /A/B/C/
// 解釋:  path.posix 使用 / 分隔符。'/' 代表根目錄。因 /A/B/C 已是完整路徑, 最終結果為/A/B/C加上/,並處理掉多餘的 /

// 6. path.posix.join() + /fetch_emp (強制 POSIX)
console.log(path.posix.join('/A/B/C', '/fetch_emp'));
// 輸出 (所有系統): /A/B/C/fetch_emp
// 解釋:  path.posix.join() 強制 / 分隔符。即使 '/fetch_emp' 開頭是 /,也會正確處理,避免雙斜線。

2025/02/21

React 19 use API 避坑指南:常見錯誤與解決方案 (含 RCC 說明)

作者:吳祐賓 

 

 


 

 

 

React 應用程式開發過程中,很常需要去 Server 端拉資料回來,通常我們會使用 useState, useEffect 來完成工作。

 

底下是一個簡單的範例,從 JSONPlaceholder (著名的公開偽資料 API 服務)提供的 API 取得使用者資料。

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React, { useState, useEffect } from 'https://esm.sh/react@18'; // Changed to React 18 for compatibility with useEffect/useState
import ReactDOMClient from 'https://esm.sh/react-dom@18/client'; // Changed to React 18

const Users = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null); // Added error handling

  useEffect(() => {
    const fetchUsers = async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/users');
      const data = await res.json();
      setUsers(data);
      setLoading(false);
    };

    fetchUsers();
  }, []); // Empty dependency array ensures this runs only once on mount

  if (loading) {
    return <h1>Loading...</h1>;
  }

  if (error) {
    return <h1>Error: {error.message}</h1>; // Display the error message
  }

  return (
    <ul>
      {users.map((user) => (
        <div key={user.id}>
          <h2>{user.name}</h2>
        </div>
      ))}
    </ul>
  );
};


function App() {
  return (
    // Suspense is not needed with useEffect + useState for initial data loading
    <Users />
  );
}

const root = ReactDOMClient.createRoot(document.getElementById('root'));
root.render(<App />);

 

 

useEffect + fetch 坊間有很多教學,因為很多人會遇到 "Warning"

 

這個設計法已經行之有年,還是有很多人搞不清楚 useEffect + fetch 為什麼會出現類似 "Warning: useEffect function must return a cleanup function or nothing. Promises..." 的訊息。

 

解決方式很多,React 19 也提出了一個解決方式。

 

 

 use API

 

use API 是 React 19 提出的新概念,是搭配 React Server Components 框架,如 Next.js 等設計使用。

 

use 的設計目標是:

 

  • 簡化 Server Components 中的資料取得設計
  • 更簡潔的程式碼和更高的可讀性
  • 錯誤處理更方便 (Error Boundaries 整合)

 

 

直接將 React 18 程式碼升級到 React 19,參考 How to fetch an API with REACT 19 | use - suspense 文章,程式碼如下:

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { Suspense, use } from 'https://esm.sh/react@19';
import ReactDOMClient from 'https://esm.sh/react-dom@19/client';
const fetchUsers = async () => {
  const res = await 
  fetch('https://jsonplaceholder.typicode.com/users');
  return res.json();
};


const Users = () => {
  const users = use(fetchUsers());

  return (
    <ul>
      {users.map((user) => (
        <div key={user.id}>
          <h2>{user.name}</h2>
        </div>
      ))}
    </ul>
  );
};


function App() {
  return (
    <Suspense fallback={<h1>Loading...</h1>}>
      <Users />
    </Suspense>
  );
}

const root = ReactDOMClient.createRoot(document.getElementById('root'));
root.render(<App />);

 

 

程式碼就是如此簡單,但 React Client Components 時會無限 Promise

 

React Stand-alone 就是 RCC 模式,在這個模式下使用 use API,就會出現如下圖般無限 Promise 的情形。

 

因為 use 是搭配 RSC 的設計,所以和 render 會有關係,RSC 框架會管理好 use 裡 Promise 的狀態,但 RCC 沒有框架處理 Promise 狀態,流程上是:

 

User render -> use(new Promise) <in use> -> do Promise -> retrun New Promise <in use> -> "use" call User render... repeat 

 

 


 

 

 RCC 的 Promise 管理快取機制自己寫,就為了 use API

 

這部份和 useCallback 有點像,只有 callback 相依的變數修改了才會更新。Promise 也可以如法泡製。參考 New React 19 use hook–deep dive 的教學,寫了 useQuery 來模擬 Server 端對 Promise 快取機制的處理。

 

程式碼修改如下。

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React, { Suspense, use, useState } from 'https://esm.sh/react@19';
import ReactDOMClient from 'https://esm.sh/react-dom@19/client';

/** useQuery Hook */
const promiseCache = new Map();

const useQuery = ({ fn, key }) => {
  if (!promiseCache.has(key)) {
    promiseCache.set(key, fn());
  }

  const promise = promiseCache.get(key);

  const result = use(promise);

  return result;
};
/* end useQuery Hook */

const fetchUsers = () => {
  const res = fetch('https://jsonplaceholder.typicode.com/users')
    .then( response => response.json());
  return res;
};

const Users = () => {
  const users = useQuery({fn: ()=> fetchUsers(), key: "fetchUsers"});

  return (
    <ul>
      {users.map((user) => (
        <div key={user.id}>
          <h2>{user.name}</h2>
        </div>
      ))}
    </ul>
  );
};


function App() {
  return (
    <Suspense fallback={<h1>Loading...</h1>}>
      <Users />
    </Suspense>
  );
}

const root = ReactDOMClient.createRoot(document.getElementById('root'));
root.render(<App />);

 

 

 

重點總結:


傳統方式 (React 18 及之前): 使用 useState 和 useEffect 組合來處理資料獲取、載入狀態和錯誤處理。  這是常見且有效的方法,但程式碼相對較多。



React 19 的 use API:


  • 設計目標: 簡化資料獲取,特別是在 Server Components (RSC) 環境下,提供更簡潔的程式碼和內建的錯誤處理。
  • RSC 環境: use hook 旨在與 React Server Components 框架 (如 Next.js) 搭配使用。 框架會負責管理 Promise 的狀態,避免無限迴圈。
  • RCC (React Client Components) 環境限制: 在獨立的 React Client Components (沒有 RSC 框架) 中直接使用 use 會導致無限迴圈,因為沒有機制來管理 Promise 的狀態。


模擬 RSC 的 Promise 快取 (RCC 環境):

 

  • 為了在 RCC 環境下也能體驗 use 的簡潔性,可以自行建立一個 Promise 快取機制 (例如範例中的 useQuery hook)。
  • useQuery hook 使用 Map 來儲存 Promise,確保相同的請求只會發送一次,避免重複的非同步操作。


 

總之,use API 是 React 19 中一個強大的新特性,但目前主要適用於 Server Components 環境。對於傳統的 Client Components,就繼續使用 useEffect 和 useState,對使用新技術有興趣的人,可以自行實作 Promise 快取機制來模擬 use 的行為。 

 

如果你正在使用 Next.js 這樣的 RSC 框架,use API 可以大幅簡化你的資料 request 的程式碼。

 

和你分享。

 

See Also