原文連結: learnreact.design/2018/01/15/…
喜歡理由: 文筆生動 通俗易懂
特別鳴謝: 原作者 Linton Ye 的傾情校對
系列部落格: 用通俗的語言和塗鴉來解釋 React 術語
- 圖解 React
- 圖解 React Native
- 元件、Props 和 State
- 深入理解 Props 和 State (本文)
- React Native vs. Cordova、PhoneGap、Ionic,等等 (待翻譯)
在上篇文章中,我們介紹了元件、props 和 state 。
props 和 state 的區別相當明顯,確定何時使用 props 和 state 似乎也很簡單。舉個例子,屋頂的顏色自然就是 prop ,因為顏色是屋頂的固有屬性。另一方面,門的開關狀態很顯然是 state ,因為門在建立後還可以開啟或關上。然而在本文中,我們將來挑戰這一思維方式!
沒開玩笑?!?沒錯,你所看到的東西既可以是 prop,又可以是 state 。並沒有絕對的界限。我將介紹一種更有用、偏實戰的方式來思考 prop 和 state 。
學習目標
當你讀完本文後希望你能重新回到這裡,並能夠輕鬆回答以下問題:
- props 和 state 的主要用途是什麼?
- “state 提升”的含義是什麼?在什麼場景下需要提升 state ?
新成員
你注意到房子周圍的新成員了嗎?試試點選房門!
檢視由 focuser (@focuser) 在 CodePen 編寫的 Demo : 有貓的 React 小屋。
這是一隻嗜睡的貓,門一關她就睡,只有當門再開啟的時候才會起來。如果把門關上,她立即又睡過去了。
實現貓
現在我來問你,如果實現貓的行為?先來試試吧!
先從下面的“程式碼”入手,花點時間先讀一遍。(再次重申,這並非真正的 JavaScript 程式碼,它只是以一種簡化的形式來幫助你理解概念,同時不會被 JS 中的細節所干擾。)
House:
<div>
<Roof />
<Wall />
<Window />
<Door />
<Cat />
</div>
Door:
State: status <!-- "open" 或 "closed" -->
<div>{state.status} door</div>
當點選門時
如果 door.state.status 為 "open"
將 door.state.status 修改成 "closed"
否則
將 door.state.status 修改成 "open"
複製程式碼
在 House
元件中又新增了 Cat
標籤。那麼 Cat
元件又是怎麼樣的呢?我們來定義它。
貓要麼睡、要麼醒。這似乎跟門的開關狀態很類似。或許我們同樣可以使用 state 來表示貓的狀態:
Cat:
State: status <!-- "sleeping" 或 "awake" -->
<div>{state.status} cat</div>
複製程式碼
Cat
元件定義好後,還需要實現的就只剩下將貓和門的狀態進行同步。門的狀態為 “open” 時,我們想要貓的狀態為 “awake”,反之為 “sleeping” 。
就這麼簡單?看看再說吧…
第一次嘗試
既然我們已經有了根據當前狀態切換門狀態的程式碼,莫不如我們就在此處切換貓的狀態:
Door:
State: status <!-- "open" 或 "closed" -->
<div>{state.status} door</div>
當點選門時
如果 state.status 為 "open"
將 state.status 修改成 "closed"
將 cat.state.status 修改成 "sleeping" <!-- 錯誤的 -->
否則
將 state.status 修改成 "open"
將 cat.state.status 修改成 "awake" <!-- 錯誤的 -->
複製程式碼
不幸的是,這不起作用!還記得元件的 state 是私有資料嗎?只有在元件的內部才能訪問。其他元件,無論是父元件還是兄弟元件,都無法訪問本元件的 state 。
很遺憾,我們在 Door
元件內嘗試修改貓的狀態以失敗告終。(轉換成真正的 JavaScript 程式碼也不例外)
第二次嘗試
那麼在 Cat
元件內來修改貓的狀態如何?這次應該可以的,是吧?
Cat:
State: status <!-- "sleeping" 或 "awake" -->
<div>{state.status} cat</div>
當點選門時 <!-- 黑人門號臉???怎麼個點選法? -->
如果 door.state.status 為 "open" <!-- 錯誤的 -->
將 cat.state.status 修改成 "sleeping"
否則
將 cat.state.status 修改成 "awake"
複製程式碼
毫無疑問,在 Cat
元件內修改貓的狀態是沒問題的。但我們需要讀取門的狀態來決定貓的狀態是什麼。門的狀態是 Door
元件的 state ,因此無法在 Cat
元件裡訪問!
解決辦法
呃!太蹩腳了。要保持門和貓的狀態同步,我們必須要在某處能同時訪問兩者。但看上去資料是通過設計而對外隱藏的。如果來解決此難題呢?
解決辦法就是需要我們靈活地理解 state 和 props 的用法。
提升門的 state
House
元件:
House:
<div>
...
<Door />
<Cat />
</div>
複製程式碼
Door
和 Cat
是並排放置的。或許這就是可以輕鬆同步它們的地方?
但是,我們現在是在 House
元件內。與之前的嘗試同理,在這裡是沒辦法讀取 Door
的 state 或者改變 Cat
的 state 。
但如果我們使用 props 來替代 state 呢?
House:
<div>
...
<Door status="open" />
<Cat status="awake" />
</div>
複製程式碼
當門關上時:
House:
<div>
...
<Door status="closed" />
<Cat status="sleeping" />
</div>
複製程式碼
當然,門的狀態不會是固定的值,它會隨時間而改變。我們用 doorStatus
來表示門的狀態。
House:
<div>
...
<Door status={doorStatus} />
<Cat status={如果 doorStatus 為 'open' 值為 'awake' 否則為 'sleeping'} />
</div>
複製程式碼
這不就解決同步的問題了嘛。順便問下,這個會變化的值 doorStatus
是什麼?在元件中什麼是可以改變的?沒錯,正是 state 。
House:
State: doorStatus <!-- 'open' 或 'closed' -->
<div>
...
<Door status={state.doorStatus} />
<Cat status={如果 state.doorStatus 為 'open' 值為 'awake' 否則為 'sleeping'} />
</div>
複製程式碼
太棒了!House
元件現在定義的很好,門和貓的狀態也能完美同步。
我們還需要修改 Door
和 Cat
元件,使用 props 來代替 state :
Door:
<div>{props.status} door</div>
Cat:
<div>{props.status} cat</div>
複製程式碼
正如你所見,因為我們想要使用來父元件的 state,在這種情況下,為了設定貓的狀態,門的狀態其實是來自於 House
的,我們可以將相同的資料表示為父元件的 state,並將資料作為 props 傳遞給子元件。通常,這被稱之為 state 提升。我們將 state 移至元件的更高層級處。
更改房子的 state
現在門和貓的狀態通過房子的 state 進行連線。如果想開門或喚醒貓的話,我們需要更改 House
元件的 state 。
問題來了,哪裡是唯一可以更新 House
的 state 的地方?就在 House
元件內,沒錯吧?
但是,我們想要在 Door
裡來觸發這次更改。也就是說,我們想要的效果是隻有當點選門時才開門,而不是點選整個房子或窗戶等。
所以 Door
元件需要做些改動:
Door:
<div>{props.status} door</div>
當點選門時
做某件事來修改 `House` 的 state
複製程式碼
但等等,之前不是說不能在 Door
元件內修改 House
的 state 嗎?
沒錯。我們沒辦法直接修改 House
的 state 。但並不等於說不能間接地修改。看下面…
在 House
元件內,我們來寫程式碼以實際修改它的 state :
House:
State: doorStatus <!-- 'open' 或 'closed' -->
toggleDoorStatus:
如果 state.doorStatus 為 'open'
將 state.doorStatus 修改成 'closed'
否則
將 state.doorStatus 修改成 'open'
...
複製程式碼
此刻,我們還未指定何時執行這段程式碼。我們只是給了它一個名字 (toggleDoorStatus
),以便稍後通過名稱來找到它執行。
然後將 toggleDoorStatus
作為 prop 傳遞給 Door
元件:
House:
...
<div>
...
<Door ... onClickAction={toggleDoorStatus} />
...
</div>
複製程式碼
在 Door
元件中,我們只需執行這個點選操作即可:
Door:
<div>{props.status} door</div>
當點選門時
執行 props.onClickAction <!-- 實際執行的是名為 "toggleDoorStatus" 的程式碼-->
複製程式碼
這就像把電視遙控器傳遞給其他人一樣。某人在 Door
元件內按下了遙控器按鍵。House
元件裡的電視機就會換臺或加大音量。
將會發生什麼取決於傳給 Door
的遙控器是什麼。它可能控制的是房間裡的電視、空調或高保真音響系統。在 Door
元件內,某人需要做的只是按下遙控器的按鍵。
這就是我們所需要的!下面是完整“程式碼”:
House:
State: doorStatus <!-- 'open' or 'closed' -->
toggleDoorStatus:
如果 state.doorStatus 為 'open'
將 state.doorStatus 修改成 'closed'
否則
將 state.doorStatus 修改成 'open'
<div>
...
<Door status={state.doorStatus} onClickAction={toggleDoorStatus} />
<Cat status={如果 state.doorStatus 為 'open' 值為 'awake' 否則為 'sleeping'} />
</div>
Door:
<div>{props.status} door</div>
當點選門時
執行 props.onClickAction
Cat:
<div>{props.status} cat</div>
複製程式碼
再次審視 Props 和 State
現在讓我們重溫幾個問題,props 和 state 的區別是什麼?何時應該使用 state ?何時應該使用 props ?
何時使用 state ? 何時使用 props ?
如果你還記得的話,我曾說過 props 是元件的固有屬性,它是不會改變的,而 state 是元件建立後才有的,它是可以改變的。當最初學習這兩個概念時,它是有幫助的。
但是,我們剛剛建立的示例讓這一觀點變得令人困惑。無論是門的開的,還是貓是睡著的,這理所應當應該是 state ,但我們卻使用 props 來表示它們。這是為什麼?
事實證明,在 state 和 props 的選擇問題上,還是有很大的靈活性的。這取決於你看它的視角,你可以採用不同的方式來為元件建模。例如,當門敞開之時,你可以說它是門的狀態,你可以說它是房子的狀態。
一種更有用的理解方式
感到困惑了?這是一種更有用的思考問題的方式:
- State: 如果 UI 需要更改就表示某處肯定會有 state
- Props: 用來傳遞資料、傳遞控制
當應用執行時,如果 UI 需要變化,那它一定是 state 。當點選門時,門或開或關,那麼它一定是某處的 state 。
但是,state 並不一定是更新元件的 state 。它可能位於某個上游元件中。這完全都取決於我們需要在何處以及如何使用這些資訊。舉個例子,我們決定將門的狀態從 Door
元件提升到 House
元件中,是因為我們需要在 House
元件中使用它。
另一方面,props 只是用來向下傳遞資料的東西。就像我們之前將門的狀態從 House
元件中向下傳遞給 Door
元件。
props 還可以用來向下傳遞控制。例如,我們將事件處理方法從 House
傳給 Door
。
本示例中的 props 是否會改變值?
並沒有,props 永遠不會改變值。我懂你的意思,門開開關關,貓睡了又醒。因為我們現在使用 props 來表示它們,很容易讓人認為 props 的行為就好比 state,它們的值改變了,是這樣嗎?
這只是一個錯覺,我發現它與翻頁書的動畫相當相似。
每次房子的 state 發生改變,舊的那隻貓就會消失,然後一隻嶄新狀態的貓將重新建立。但是這一過程發生的非常之快,這就造成了我們的視覺殘留,認為只有一隻貓在那睡了又醒。
翻書中任何頁面上的草圖都不會移動。類似的,每隻貓在其(短暫的)生命中始終保持清醒/沉睡。
總結
好了,我們通過一個更復雜的示例再次學習了 props 和 state 。在這個示例中,當點選門時,門需要切換開關狀態,同時我們還需要將門和貓的狀態保持同步,
因為 state 是私有的,所以我們需要將門的狀態從 Door
元件提升到 House
元件中。這樣我們便可以在 House
元件中使用此資料來設定門和貓的狀態。我們將此資料作為 props 傳給 Door
和 Cat
,以便它們根據門的狀態來顯示正確的圖片。
另一個需求是點選門時觸發狀態的變更。因為現在門的狀態是 House
的 state,它是私有資料,只能在 Door
元件中間接來更改它。我們在 House
元件中編寫了實際修改 state 的程式碼,然後將其作為 props 傳遞給 Door
。這類似於把電視的遙控器傳給別人。
本文中的示例或許會讓你感到一絲困惑。下面是一種用來思考 prop 和 state 的更實用的方式:
- State: 如果 UI 需要更改就表示某處肯定會有 state
- Props: 用來傳遞資料、傳遞控制
到目前為止感覺如何?如果你有任何問題或意見,請給我留言!