React integration(在React中集成MobX)
用法:
import { observer } from "mobx-react-lite" // Or "mobx-react".
const MyComponent = observer(props => ReactElement)
雖然MobX獨(dú)立于React工作,但它們通常一起使用,在The gist of MobX中,你已經(jīng)見到了集成中最重要的部分:可以封裝React組件的observer 高階組件。
observer由安裝過程中所選擇的單獨(dú)的React bundle提供。在本例子中,我們將使用更輕量級的mobx-react-litepackage。
import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react-lite"
class Timer {
secondsPassed = 0
constructor() {
makeAutoObservable(this)
}
increaseTimer() {
this.secondsPassed += 1
}
}
const myTimer = new Timer()
// 用`observer`包裝的函數(shù)組件將對每一個引用的可觀察對象的每一個變化做出反應(yīng)。
const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
setInterval(() => {
myTimer.increaseTimer()
}, 1000)
提示:你可以在CodeSandbox嘗試上面的例子。
observer高階組件會自動訂閱React組件渲染過程中使用的每一個可觀察對象,當(dāng)相關(guān)的可觀察對象發(fā)生變化時,組件將自動重新渲染。同時,它還確保了組件在沒有相關(guān)更改時不會重新渲染,因此,組件可訪問但沒被實(shí)際讀取的可觀察對象永遠(yuǎn)不會導(dǎo)致重新渲染。
在實(shí)踐中,這使得MobX應(yīng)用程序得到了很好的開箱即用優(yōu)化,它們通常不需要任何額外的代碼來防止過度渲染。
對于observer而言,可觀察對象如何到達(dá)組件并不重要,只要在組件中直接讀取就好。像todos[0].authors.displayName這樣深度讀取可觀察對象的復(fù)雜表達(dá)式也可以直接使用。與其他必須顯式聲明數(shù)據(jù)依賴關(guān)系,或預(yù)先計算數(shù)據(jù)依賴關(guān)系(例如選擇器)的框架相比,MobX的訂閱機(jī)制更加精確和高效。
內(nèi)部狀態(tài)和外部狀態(tài)
state的組織方式有很大的靈活性,盡管(從技術(shù)上講)我們讀取到的是什么可觀察對象,或者可觀察對象來自哪里并不重要。外部和內(nèi)部可觀察狀態(tài)在由observer包裝的組件中使用的不同方式示例如下。
在observer組件中使用外部狀態(tài)
- 通過props參數(shù)
// 被觀察對象可以作為props傳遞到組件中
import { observer } from "mobx-react-lite"
const myTimer = new Timer() // 請參見上面的計時器定義
const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)
// 將myTimer作為props傳遞
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
- 通過全局變量
如何獲得可觀察對象的引用并不重要,因此我們可以直接使用外部作用域中的可觀察對象
const myTimer = new Timer() // 請參見上面的計時器定義
// 沒有props,`myTimer`直接從外部作用域獲取
const TimerView = observer(() => <span>Seconds passed: {myTimer.secondsPassed}</span>)
ReactDOM.render(<TimerView />, document.body)
直接使用可觀察對象非常有效,但由于這通常會引入module state,因此此模式可能會使單元測試變得復(fù)雜。相反,我們建議改用React Context。
- 使用React Context
React Context是與整個子樹共享可觀測對象的一種很好的機(jī)制:
import {observer} from 'mobx-react-lite'
import {createContext, useContext} from "react"
const TimerContext = createContext<Timer>()
const TimerView = observer(() => {
// 在上下文中提取timer
const timer = useContext(TimerContext) // 請參見上面的計時器定義
return (
<span>Seconds passed: {timer.secondsPassed}</span>
)
})
ReactDOM.render(
<TimerContext.Provider value={new Timer()}>
<TimerView />
</TimerContext.Provider>,
document.body
)
請注意,我們不建議手動更新Provider的value。或者說使用MobX時不需要這樣做,因?yàn)楣蚕淼目捎^察對象可以自己更新。
在observer組件中使用內(nèi)部狀態(tài)
observer使用的可觀察對象也可以是內(nèi)部狀態(tài)。同樣,我們可以有幾種不同的選擇。
- 以可觀察類的方式使用
useState
使用本地可觀察狀態(tài)的最簡單方法是使用useState存儲對可觀察類的引用。請注意,我們通常不想替換引用,因此完全可以忽略useState返回的更新函數(shù)。
import { observer } from "mobx-react-lite"
import { useState } from "react"
const TimerView = observer(() => {
const [timer] = useState(() => new Timer()) // 請參見上面的計時器定義
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
如果想要像原始示例那樣自動更新計時器,則可以使用useEffect:
useEffect(() => {
const handle = setInterval(() => {
myTimer.increaseTimer()
}, 1000)
return () => {
clearInterval(handle)
}
}, [myTimer])
2.以可觀察對象的方式使用useState
如前所述,我們可以用 observable 方式直接創(chuàng)建觀察對象,而不是使用類。
import { observer } from "mobx-react-lite"
import { observable } from "mobx"
import { useState } from "react"
const TimerView = observer(() => {
const [timer] = useState(() =>
observable({
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
}
})
)
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
3.使用useLocalObservable hook
像 const [store] = useState(() => observable({ /* something */}))這樣的代碼非常常見,使用mobx-react-lite包中的useLocalObservable hook可以將上面的示例簡化為:
import { observer, useLocalObservable } from "mobx-react-lite"
import { useState } from "react"
const TimerView = observer(() => {
const timer = useLocalObservable(() => ({
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
}
}))
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
你可能不需要內(nèi)部可觀察狀態(tài)
一般來說,我們建議不要輕易使用MobX observable來獲取局部組件狀態(tài),因?yàn)閺睦碚撋现v,這可能會讓你無法使用React Suspense機(jī)制的某些特性。最佳實(shí)踐是,當(dāng)狀態(tài)捕獲組件(包括子組件)之間共享域數(shù)據(jù)時,使用MobX observable。例如待辦事項(xiàng)、用戶、預(yù)訂等。
只捕捉UI的狀態(tài),如加載狀態(tài)、選擇狀態(tài)時,使用useState hook可能會更好,因?yàn)檫@將允許你在未來利用React Suspense的某些特性。
總是在observer組件內(nèi)部讀取可觀察對象
你可能想知道,該什么時候使用observer。最佳實(shí)踐是:把observer應(yīng)用到所有你希望讀取可觀察對象的組件中
observer只增強(qiáng)被他裝飾的組件,而不是它調(diào)用的組件。所以通常所有的組件都應(yīng)該被observer包裝。不要擔(dān)心。。這并不會降低效率。相反,更多的、更精細(xì)的observer組件會使渲染變得更高效。
提示:盡可能晚地從對象中獲取值
如果你想盡可能長時間地傳遞對象引用,并且只在基于observer的組件中讀取它們的屬性,并將它們渲染到DOM或子組件中,那么observer的工作效果最好。
在上面的例子中,如果TimerView 組件定義如下,它將不會對未來的更改做出反應(yīng),因?yàn)? .secondsPassed不是在observer組件內(nèi)部讀取的,而是在外部讀取的,因此不會被跟蹤:
const TimerView = observer(({ secondsPassed }) => <span>Seconds passed: {secondsPassed}</span>)
React.render(<TimerViewer secondPassed={myTimer.secondsPassed} />, document.body)
請注意,這是一種不同于Reaction-Redux等其他庫的思維方式,在Reaction-Redux庫中,提前取消引用并向下傳遞原語是一種很好的做法,以更好地利用內(nèi)存。如果不能完全理解,請查看Understanding reactivity部分
`
不要將observable傳遞給非observer的組件
用observer包裝的組件只訂閱在它們自己渲染組件時使用到的可觀察對象。因此,如果可觀察對象/數(shù)組/映射被傳遞給子組件,它們也必須被包裝為observer。這也適用于任何基于回調(diào)的組件。
如果你希望將observer對象傳遞給非observer的組件(因?yàn)樗堑谌浇M件,或者因?yàn)槟M3衷摻M件的MobX不可知),則必須在傳遞之前 將可觀察對象轉(zhuǎn)換為普通的JavaScript值或結(jié)構(gòu)。
為了進(jìn)一步說明上面的問題,來看看下面可觀察的todo對象的例子:一個TodoView組件(觀察者)和一個虛構(gòu)的GridRow組件,它接受列 / 值映射,但未使用 observer:
class Todo {
title = "test"
done = true
constructor() {
makeAutoObservable(this)
}
}
const TodoView = observer(({ todo }: { todo: Todo }) =>
// 錯誤:GridRow不會獲取todo.title/todo.do中的更改,因?yàn)樗皇怯^察者
return <GridRow data={todo} />
// 正確:讓`TodoView`檢測`todo`中的相關(guān)變化,并向下傳遞普通數(shù)據(jù)。
return <GridRow data={{
title: todo.title,
done: todo.done
}} />
// 正確:使用`toJS`也可以,但顯式通常更好。
return <GridRow data={toJS(todo)} />
)
回調(diào)組件可能需要<Observer>
想象一下同樣的例子,GridRow使用了一個onRender回調(diào)函數(shù)。因?yàn)閛nRender是GridRow的渲染周期的一部分,而不是由TodoView渲染。所以我們必須確?;卣{(diào)組件使用了observer組件。或者,我們可以使用 <Observer />創(chuàng)建一個匿名observer。
const TodoView = observer(({ todo }: { todo: Todo }) => {
// 錯誤:GridRow不會獲取todo.title/todo.do中的更改,因?yàn)樗皇怯^察者
return <GridRow onRender={() => <td>{todo.title}</td>} />
// 正確:將回調(diào)包裝在觀察器中,以便能夠檢測更改。
return <GridRow onRender={() => <Observer>{() => <td>{todo.title}</td>}</Observer>} />
})