檔案列表要支援重新命名。我在 FileRow 裡寫了這段:
interface FileRowProps {
file: File;
}
export function FileRow({ file }: FileRowProps) {
function handleRename(newName: string) {
file.name = newName;
}
return <input defaultValue={file.name} onBlur={e => handleRename(e.target.value)} />;
}
改完離開輸入框。畫面沒動,父層的清單也沒動,什麼都沒發生。
我的第一個念頭是:「又是昨天那個 Object.is 吧,我改了物件內容但參考沒換。」
不對。這次的問題更前面一點——那份資料根本不歸我管,我從一開始就不該在這裡改它。
往下傳跟往上傳,兩邊其實長得很像:
// Angular v20
export class FileRowComponent {
file = input.required<File>();
renamed = output<string>();
}
<app-file-row [file]="f" (renamed)="onRenamed($event)" />
React 那邊也是一個傳值、一個傳函式。這部分沒什麼好說的。
真正的差別有兩個。
第一個是雙向綁定。
<input [(ngModel)]="keyword" />
一行。值會自動填進去,使用者打字也會自動寫回 keyword。你不用寫任何一條接著的方法。
自訂元件也能做到,只要 input 跟 output 湊成一對:
<app-search-box [(value)]="keyword" />
第二個更關鍵:物件 input 傳下去的是同一個參考。
export class FileRowComponent {
file = input.required<File>();
rename(newName: string) {
this.file().name = newName; // 直接改,父層的畫面會跟著更新
}
}
這能動,而且動得很順。因為變更偵測走訪整棵樹的時候,會重新讀取每個綁定的值——它不在乎是誰改的,只在乎現在的值跟上次一不一樣。
所以在 Angular,父層和子層其實在操作同一份東西。方便,但也代表當一個值莫名其妙變了的時候,嫌疑犯是整棵樹上的每一個元件。
React 這邊只有一條規則:
props 是唯讀的。不是慣例,是規則。
function FileRow({ file }) {
file.name = 'new'; // ❌ 不要這樣做
}
這行不會報錯,JavaScript 也擋不住你——物件參考傳進來了,你當然改得動。但它不會觸發任何更新,因為 React 沒有在偵查(這是昨天的結論)。它只認 setState,而 setState 在父層手上。
改了 props,你只是弄髒了別人的東西,而且沒人會知道。
所以資料要往上,只有一條路:父層把函式傳下來,子層呼叫它。
function FileRow({ file, onRename }: {
file: File;
onRename: (id: string, name: string) => void;
}) {
return (
<input
defaultValue={file.name}
onBlur={e => onRename(file.id, e.target.value)}
/>
);
}
function FileList() {
const [files, setFiles] = useState<File[]>([]);
function handleRename(id: string, name: string) {
setFiles(prev => prev.map(f => f.id === id ? { ...f, name } : f));
}
return files.map(f => (
<FileRow key={f.id} file={f} onRename={handleRename} />
));
}
心智模型的差別在這裡:
Angular:父子共享同一份資料,資料沒有擁有者,拿得到就改得動。
React:子元件收到的是一份唯讀的副本,想改就去通知資料擁有者。
FileRow 從頭到尾不知道那份資料存在哪、會被怎麼改。它只知道「有人給了我一個 file,還給了我一個回報改資料的方法」。
單向資料流最有感的地方,是表單。
Angular:
<input [(ngModel)]="keyword" />
React:
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
/>
一個往下(value),一個往上(onChange)。
第一週我每寫一個 input 就在心裡嘆一次氣。但寫到第二十個的時候,我突然意識到一件事:
[(ngModel)] 做的也是這兩件事。
那個香蕉盒語法根本不是什麼特殊機制,它就是 [ngModel] 加上 (ngModelChange) 的語法糖。Angular 幫你把兩條線綁成一條,React 只是把它拆開來攤在你眼前。
[(ngModel)]="keyword"
≡ [ngModel]="keyword" (ngModelChange)="keyword = $event"
這種元件在 React 叫受控元件(controlled component)——它的值由 state 控制,自己不保存任何東西。你也可以選擇不控制它,讓 DOM 自己管值(就是上面那個 defaultValue),但那是另一個主題了。
大型表單怎麼處理,Week 3 會專門講。今天的重點只有一個:React 沒有把方向合併起來的語法,因為它不打算讓你忘記有兩個方向。
單向資料流會逼你回答一個 Angular 不太需要面對的問題:這份資料應該住在誰身上?
因為資料只能往下流,所以規則很單純:
需要這份資料的元件們,它們最近的共同祖先就是家。
舉例:搜尋框跟結果列表是兄弟,兩個都要用到 keyword。那 keyword 就不能住在搜尋框裡,得提升到它們的父層。
這個動作叫狀態提升,是 React 最常見的重構之一。
在 Angular 你比較少做這件事,因為你有別的路:注入一個 service 就好。兩個毫不相干的元件都 inject(SearchService),資料自然共享,誰都不用改結構,平常在寫專案我也會把模組常用API放在service。
這就是 Day 2 那套 DI 的另一個用途——Angular 的 service 同時也是狀態共享的管道。
假設 keyword 提升到了頁面層,而真正要用它的元件在三層之下:
<FilePage> ← keyword 住這裡
<Toolbar keyword={...}> ← 不需要,只是路過
<FilterBar keyword={...}> ← 不需要,只是路過
<SearchBox keyword={...} /> ← 真正要用的人
中間那兩層完全不關心 keyword,卻不得不接下來再傳下去。這就是 prop drilling。
代價不只是打字。真正的成本是:
React 的解法有兩條:Context 可以讓子樹直接取用,但一變動整棵訂閱的子樹都會重繪(昨天那個 re-render 的故事),而且我在 Day 2 已經說過它的成本;另一條是狀態管理套件,讓資料脫離元件樹單獨存在——這個下週會專門講。
老實說,這是我目前覺得 React 最麻煩的地方。 在 Angular,一個 providedIn: 'root' 的 service 就解決了,不用思考誰是共同祖先、不用煩惱要不要引入 Context、不用擔心整棵子樹重繪。
寫到這裡都在講代價,該給單向資料流一個公道。
它真正的價值,在出事的時候。
當一個值莫名其妙變了,React 專案裡你只需要問一個問題:誰呼叫了那個 setState? 資料只有一個來源、只有一條流向,往上追一定追得到。
同樣的問題在 Angular 就不一定。如果那是一個被層層傳遞的物件,任何一個拿到它的元件都可能動過手;如果用了雙向綁定,值的變動可能發生在你根本沒看的那個檔案裡。
小專案感受不到差別,會覺得 Angular 比較好用,但其實也見人見智啦,對我來說一定是熟悉語法最好用。但在一個五十個元件、三年歷史、換過四手的企業系統裡,「這個欄位為什麼變成這樣」會從除錯變成考古。
React 用樣板換來的,是資料流的可追溯性。它不相信你會自律,所以直接把另一個方向的路封起來。
Angular 則是給你自由,順便給你一整套規範跟 code review 文化去約束它。兩邊都能寫出好系統,只是紀律的來源不同——一個靠團隊,一個靠框架。
@Input/@Output 其實有對應物,props 跟 callback 幾乎是一對一。真正不見的是雙向的路。
Angular 讓資料可以兩頭流動,父子共享同一份東西,改哪裡都算數。方便,但當系統長大,你得靠團隊紀律來維持秩序。甚至會不想用這麼多父子組合
React 只給你一個方向。子元件永遠只是資料的讀者,想改就得往上通報。代價是狀態提升、prop drilling、每個 input 兩條線——但換來的是,任何一個值的變動都有跡可循。
那個沒反應的 rename,不是 React 的 bug,是我還在用「大家共用一份資料」的方式寫程式。在 React 的世界裡,每一份資料都有明確的擁有者,而我當時不是。
Angular 相信你不會亂改,React 乾脆不給你機會。