自從我們學習React的第一天起,我們就知道不要在React中使用太多狀態。我們也知道應該儘可能多使用無狀態的函數式組件,少使用有狀態的類組件。
這些建議很容易讓我們形成這樣的判斷,那就是我們完全不應該使用狀態,當我們需要用到狀態的時候,應該使用Redux之類的第三方狀態管理庫。
我們不喜歡使用React原生狀態並不是沒有原因的:
- 當項目變大的時候,散落在許多組件中的局部狀態很快會變得無法追蹤;
- 組件之間難以共享狀態;
- 從一個外部組件一層層將prop傳遞到內部組件很麻煩;這個問題被稱作prop drilling;
- 狀態邏輯通常和UI邏輯混合在一起,導致調試和重用狀態邏輯都很困難;
- 在編譯時,類組件比函數式組件更難優化。
嚴格來講,prop drilling並不屬於狀態的問題,而是一個設計失誤。它通常是由過多的不必要封裝導致的。
Redux的優缺點
Redux是最流行的狀態管理庫,它通過將狀態與UI隔離並中心化管理的方式減輕了以上所說的這些痛點。
它的整個邏輯十分簡單,但卻蘊含着強大的擴展性。我們可以用一個單向數據流圖來描述Redux背後的運行機制:
除了store自身,所有其他的組件都是純函數。這是一個函數式編程中的概念,指的是函數的結果只取決於函數的輸入,而不取決於任何其他的狀態。
純函數更容易測試、理解和調試。通過強制使用函數式編程這一編程風格,Redux減少了維護狀態邏輯的負擔。
然而,Redux也有自己特有的麻煩。一個人們廣爲詬病的問題就是在搭建項目初期,它需要寫太多樣板代碼。
對於小項目而言,這個問題尤爲嚴重。添加一個新的action通常需要我們定一個新action、添加一個新action creator、修改reducer、更新container等一系列操作。爲了完成這個功能,我們需要在許多個不同的文件之間跳轉,最後把一個5行之內可以做完的事情擴展成20行。
甚至說,我們到底要不要中心化的store也是一個值得討論的問題,就像我們要不要用全局變量一樣。
和局部狀態相比,全局狀態更難重用。重構全局狀態可能會意外地導致部分現有代碼不可用。不合理地使用Redux container也會有性能問題。
在另一邊,近幾年React爲了讓狀態管理更好用做出了許多努力,引入了Context API和Hooks等新特性來解決過去的痛點。
原生React
React支持兩種組件:類組件(支持狀態和hook,也就是componentDidMount等函數)和函數式組件(不支持狀態,更加簡單)。
在過去,如果我們想使用函數式組件,又想維護某些內部狀態,唯一的辦法就是使用Redux之類的狀態管理庫。
在React增加了Context API和hooks之後,我們有了更多的選擇。
因爲這兩個新API是React官方支持的,我建議在使用第三方的狀態管理庫之前,先了解一下它們再做決定。
React Hooks
React hooks API提供了一種在函數式組件中管理狀態的方法。
這句話第一眼看上去自相矛盾,因爲函數式在某種程度上就意味着無狀態。但是,我們先將這一問題擱置在一邊,只把函數式組件當作一種設計模式來看待。
和類組件相比,函數式組件有更緊湊的形式,需要更少的樣板代碼,更可讀,對編譯器來說也更容易分析和優化。
然而,不能夠在函數式組件中維護狀態爲我們帶來了極大的不便:即使是引入一個boolean這樣小的狀態,都需要我們將整個函數式組件重寫爲類組件。
React hooks API通過允許我們在函數式組件中使用狀態來解決了這個困境。而且,它也允許我們將狀態邏輯從UI邏輯中剝離出來,重用到其他的UI組件中。
下面這個簡短的例子展示了我們如何在函數式組件中使用React hooks:
import React, { useState } from 'react'
const Counter: React.FC = () => {
const [counter, setCounter] = useState(0)
return (
<div>
<p>Counter: {counter}</p>
<button onClick={() => setCounter(counter + 1)}>
Increment
</button>
</div>
)
}
useState返回當前狀態和一個函數(這個函數可以用來更新狀態),它的參數是初始狀態。這個函數第一次被調用的時候,它將組件的內部狀態初始化爲給定的初始值。
這個狀態是維護在這個組件之內的,不會被同一個組件的多個實例之間共享。
我們可以在一個函數式組件中調用useState許多次。React hooks API鼓勵我們將一個複雜的狀態分解成多個可重用的簡單狀態。
我們可以進一步將狀態邏輯封裝在單獨的函數中(稱爲custom hook),以便我們在其他組件中重用這個狀態邏輯:
import React, {useState} from 'react'
// A custom hook
const useCounter = (initial: number) => {
const [counter, setCounter] = useState(initial)
return {
counter,
increment () {
setCounter(counter + 1)
},
reset () {
setCounter(initial)
}
}
}
const Counter: React.FC = () => {
const { counter, increment, reset } = useCounter(0)
return (
<div>
<p>Counter: {counter}</p>
<button onClick={increment}>
Increment
</button>
<button onClick={reset}>
Reset
</button>
</div>
)
}
每一個使用setState的地方,我們都可以用React hooks代替,因爲React hooks全方面地超越了原來的setState:它允許將一個組件的狀態拆分成小的可重用的狀態,鼓勵我們多用函數式組件,少用類組件。
React Context API
React Context API比React hooks更早引入React,但是它是用來解決一個完全不同的問題的:狀態共享和prop drilling。
這個功能可能會讓你聯想到Redux的用途,但是React Context API事實上並不鼓勵用它來維護一個巨大的中心化store。官方文檔中說到:
Context主要是用在數據需要被不同層次的多個組件訪問的時候。儘可能少用它,因爲它會使組件重用更加困難。
React Context API和React hooks的設計哲學是相同的,那就是儘可能避免狀態共享。
我們不得不使用React Context API的典型場景有:
- 用戶登錄信息;
- UI主題;
- locale設置。
例如,我們可以通過把UI主題放到Context中的方法來避免手動逐層傳遞:
import React, { useContext } from 'react'
const ThemeContext = React.createContext('light')
const UserComponent: React.FC = () => {
const theme = useContext(ThemeContext)
return (
<div>
Current theme: {theme}
</div>
)
}
const App: React.FC = () => {
return (
<ThemeContext.Provider value="dark">
<UserComponent />
</ThemeContext.Provider>
)
}
通過合理地組合React Context API和React hooks,我們可以在完全不用Redux的情況下管理程序狀態。
然而,就像編程世界中的其他開放性問題一樣,狀態管理也沒有萬金油。具體怎麼做取決於業務邏輯的複雜性、程序的規模和其他各種因素。
我們應該在實際中選擇最合適的方法。我的建議是,在項目初期,可以使用Context API和React hooks作爲開始,隨着項目的進行,只在必要的時候才引入Redux。