Use Functional Lenses with ReactJS

在 React Component 中,我們能藉由 this.setState 輕而易舉的改變 Component 的狀態。但是當你的 state 是個深層 Object,要去更新 state 中的某些數值就變得有些難以處理。

而在 React 裡,對待 state 必須是 Immutable 的,若是直接修改 Object 如:

let user = this.state.user
user.name = 'Alexa'
this.setState({ user })

會因為 JavaScript Object 的 reference 仍然是相同的,而造成在 shouldComponentUpdate lifecycle 無法比對 state 的不同,而不會更新介面。

在這一篇,將介紹 Functional Lens 可以如何在 React 中以 Immutable 的方式做到深層 state 的更新。


讓我們先來點簡單的 React Component:

這看起來沒什麼問題,state 很簡單,僅有一個 property,也就是 count<Number> ,想要改變 state 也只需要簡單易讀的一行 function 就能做到增加或減少 count 值。

但是當 state 的結構是這樣呢:

state = {
city: {
name: 'Fantasy',
people: {
human: 107,
furry: 23
}
}
}

相比前一個來得複雜一些,但這看起來也還好,邏輯也很容易理解呢。那麼,現在我們要來改變一下 human的數值。為了增加 human的數字,必須也改變 citypeople 的值。那我們的這個 function 會看起來像這樣:

這真的看起來不好閱讀呢…

setState 這個 function 會將之前的 state 與現在新丟出的 state 做 merge,但它並不會做深層的 merge,要是去掉 ...state.city 與 ...state.city.people ,確實 human 更新成新的值了,但 state 也就會變成:

{
city: {
people: {
human: 108,
}
}
}

就只會剩下上次更新的 human 。所以才會在 city 裡的每一層都進行 ...object 的行為。

同樣的 decreaseHuman 也需要這樣做。所以我們現在的 Component 會變成:

這給人感覺有點亂啊,要是 state 結構再更加複雜,要看懂就要花上不少時間,當然這邊有更好的作法。

Lenses

簡單來說, Lenses 就是一個 functional getter/setter ,基本功能就是存取以及更新 Object ,在 JavaScript 中大致可以這樣呈現一個簡單的 Lenses:

正如字面上所寫,如透鏡一般讓你 “專注在” state 中特定一個 property。使用 lens 專注在 state 上的某個 property 後,你可以賦予這個 property 新的值,而不會遺失 state 中的其他資料。

這邊以 RamdaJS library 提供的 function 來示範。首先使用 lensPath 這個 function 來建立專注在 human 上的 lens:

現在我們有了專注在 human 上的 lens,接者可以使用 RamdaJS 中提供給 lens 使用的 function,ex: view , set , over :

如範例,藉由 lensview ,可以直接從 state 中直接取得 human 的數值 107 。當然, view 這個 function 在這看起來並沒有特別有用,接著就來看看 set function 的作用:

藉由上面行為, set 會回傳一個新的 state object:

{
city: {
name: 'Fantasy',
people: {
human: 20,
furry: 23,
}
}
}

可以看到 human 的值已被變換成 20 。這不僅得到我們要的結果,同時也是以 Immutable 的方式達成了。

我們可以試著在我們的 Component 上使用, increaseHuman 會變成如此:

這邊我們藉由 view function 拿到當前 human 的數值,再使用 set function 將當前 state 中的 human 更新為增加後的值。

接著讓我們看看 over 這個 function 。與 set 不同, over 的第二個參數能代入的是一個 function ,而這個 function 的傳入值就像是從 view 行為裡獲得的,回傳值則像是 set 把結果丟進 state 裡面。

如上,over 就是有點像 view + set 的行為。

這樣看起來不是好看多了,同樣 decreaseHuman 也可以比照辦理。

理所當然,還可以做得更加簡化。在 RamdaJS 中所有的 function 都有著 Auto-Curried 的性質。也就是說, over 可以這麼用:

這樣便能把 setState 簡化成:

經過多次修改後,讓 component 裡面的複雜度降低不少,相對來說比較易讀許多。此外也可以在 render 時使用 view ,也就是說以 
view(humanLens, this.state) 取代 this.state.city.people.human ,使 code 變得整齊許多。

另外,RamdaJS 提供的 Lenses 可以拿來 compose 。也就是說,我們可以這麼做:

這樣的特性讓 Lenses 可以進行大幅度的再利用。

到這邊,我們可以看見,在巢狀 Object 結構中, Lenses 提供我們一個相對簡單乾淨的方法來更新和存取 Object 的內容,而現實中的 state 肯定比範例中複雜不少。

以下就是這次範例的最終版本,歡迎隨時拿去試看看。