React 整合
用法
import { observer } from "mobx-react-lite" // Or "mobx-react".
const MyComponent = observer(props => ReactElement)
雖然 MobX 可以獨立於 React 運作,但它們最常一起使用。在 MobX 的核心概念 中,您已經看到了這個整合最重要的部分:您可以包裝在 React 元件周圍的 observer
高階元件 (HoC)。
observer
由您在 安裝過程中 選擇的獨立 React 綁定套件提供。在此範例中,我們將使用更輕量級的 mobx-react-lite
套件。
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()
// A function component wrapped with `observer` will react
// to any future change in an observable it used before.
const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
setInterval(() => {
myTimer.increaseTimer()
}, 1000)
提示: 您可以在 CodeSandbox 上自行體驗上述範例。
observer
高階元件會自動將 React 元件訂閱到在渲染期間使用的任何可觀察物件。因此,當相關的可觀察物件發生變化時,元件將自動重新渲染。它還能確保在沒有相關變化時,元件不會重新渲染。因此,元件可以存取但實際上未讀取的可觀察物件永遠不會導致重新渲染。
實際上,這使得 MobX 應用程式在開箱即用的情況下就得到了很好的優化,而且它們通常不需要任何額外的程式碼來防止過度渲染。
為了讓 observer
能夠運作,可觀察物件如何到達元件中並不重要,重要的是它們是否被讀取。深度讀取可觀察物件是可以的,像 todos[0].author.displayName
這樣的複雜表達式也能夠直接使用。與其他必須明確宣告或預先計算資料依賴關係的框架(例如選擇器)相比,這使得訂閱機制更加精確和有效率。
區域和外部狀態
狀態的組織方式非常靈活,因為讀取哪些可觀察物件或可觀察物件來自哪裡(技術上來說)並不重要。以下範例示範了如何在使用 observer
包裝的元件中使用外部和區域可觀察狀態的不同模式。
observer
元件中使用外部狀態
在 可觀察物件可以作為 props 傳遞到元件中(如上例所示)
import { observer } from "mobx-react-lite"
const myTimer = new Timer() // See the Timer definition above.
const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)
// Pass myTimer as a prop.
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
由於我們如何獲得可觀察物件的參考並不重要,我們可以直接從外部作用域(包括從導入等)使用可觀察物件
const myTimer = new Timer() // See the Timer definition above.
// No props, `myTimer` is directly consumed from the closure.
const TimerView = observer(() => <span>Seconds passed: {myTimer.secondsPassed}</span>)
ReactDOM.render(<TimerView />, document.body)
直接使用可觀察物件效果很好,但由於這通常會引入模組狀態,因此這種模式可能會使單元測試複雜化。相反,我們建議使用 React Context。
React Context 是一個很好的機制,可以與整個子樹共享可觀察物件
import {observer} from 'mobx-react-lite'
import {createContext, useContext} from "react"
const TimerContext = createContext<Timer>()
const TimerView = observer(() => {
// Grab the timer from the context.
const timer = useContext(TimerContext) // See the Timer definition above.
return (
<span>Seconds passed: {timer.secondsPassed}</span>
)
})
ReactDOM.render(
<TimerContext.Provider value={new Timer()}>
<TimerView />
</TimerContext.Provider>,
document.body
)
請注意,我們不建議將 Provider
的 value
替換為不同的值。使用 MobX,應該不需要這樣做,因為共享的可觀察物件本身可以更新。
在 `observer` 元件中使用區域可觀察狀態
由於 `observer` 使用的可觀察物件可以來自任何地方,它們也可以是區域狀態。同樣,我們可以使用不同的選項。
使用區域可觀察狀態最簡單的方法是使用 `useState` 儲存對可觀察類別的參考。請注意,由於我們通常不想替換參考,因此我們完全忽略了 `useState` 返回的更新器函式
import { observer } from "mobx-react-lite"
import { useState } from "react"
const TimerView = observer(() => {
const [timer] = useState(() => new Timer()) // See the Timer definition above.
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
如果您想像在原始範例中那樣自動更新計時器,可以使用典型的 React 方式使用 `useEffect`
useEffect(() => {
const handle = setInterval(() => {
timer.increaseTimer()
}, 1000)
return () => {
clearInterval(handle)
}
}, [timer])
如前所述,可以不使用類別,而是直接創建可觀察物件。我們可以使用 observable 來做到這一點
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)
`const [store] = useState(() => observable({ /* something */}))` 這種組合相當常見。為了簡化這種模式,`mobx-react-lite` 套件提供了 `useLocalObservable` hook,可以將前面的範例簡化為
import { observer, useLocalObservable } from "mobx-react-lite"
const TimerView = observer(() => {
const timer = useLocalObservable(() => ({
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
}
}))
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
您可能不需要區域可觀察狀態
一般來說,我們建議不要過快地將 MobX 可觀察物件用於區域元件狀態,因為這在理論上可能會使您無法使用 React Suspense 機制的一些功能。根據經驗,當狀態捕獲在元件(包括子元件)之間共享的網域資料時,請使用 MobX 可觀察物件。例如待辦事項、使用者、預訂等。
僅捕獲 UI 狀態(例如載入狀態、選擇等)的狀態可能更適合使用 `useState` hook,因為這將允許您在未來利用 React Suspense 功能。
在 React 元件中使用可觀察物件,只要它們 1) 很深,2) 具有計算值,或 3) 與其他 `observer` 元件共享,就會增加價值。
始終在 `observer` 元件內讀取可觀察物件
您可能會想知道,我什麼時候應該應用 `observer`?經驗法則是:*將 `observer` 應用於所有讀取可觀察資料的元件*。
`observer` 只會增強您正在裝飾的元件,而不是它調用的元件。因此通常所有元件都應該用 `observer` 包裝。別擔心,這不會降低效率。相反,更多的 `observer` 元件會使渲染更有效率,因為更新會變得更細粒度。
提示:盡可能晚地從物件中獲取值
如果您盡可能長時間地傳遞物件參考,並且僅在要將它們渲染到 DOM / 低階元件的基於 `observer` 的元件內讀取它們的屬性,則 `observer` 的效果最佳。換句話說,`observer` 會對您從物件中「解引用」一個值的事實做出反應。
在上面的範例中,如果 `TimerView` 元件定義如下,它將*不會*對未來的更改做出反應,因為 `.secondsPassed` 不是在 `observer` 元件內讀取的,而是在外部讀取的,因此*沒有*被追蹤
const TimerView = observer(({ secondsPassed }) => <span>Seconds passed: {secondsPassed}</span>)
React.render(<TimerView secondsPassed={myTimer.secondsPassed} />, document.body)
請注意,這與其他函式庫(例如 `react-redux`)的思維方式不同,在 `react-redux` 中,良好的做法是及早解引用並將基本類型向下傳遞,以更好地利用記憶體。如果問題不是很清楚,請務必查看理解反應性章節。
不要將可觀察物件傳遞到非 `observer` 的元件中
使用 `observer` 包裝的元件*僅*訂閱在其*自身*渲染元件期間使用的可觀察物件。因此,如果將可觀察物件 / 陣列 / 地圖傳遞給子元件,則這些子元件也必須使用 `observer` 包裝。這也適用於任何基於回調的元件。
如果您想將 observables 傳遞給非 observer
的組件,或者因為它是第三方組件,或者因為您想讓該組件與 MobX 無關,則必須在傳遞它們之前將 observables 轉換為普通的 JavaScript 值或結構。
為了詳細說明上述內容,請參考以下範例 observable todo
物件、一個 TodoView
組件(observer)和一個虛擬的 GridRow
組件,它接受欄位/值映射,但它不是 observer
class Todo {
title = "test"
done = true
constructor() {
makeAutoObservable(this)
}
}
const TodoView = observer(({ todo }: { todo: Todo }) =>
// WRONG: GridRow won't pick up changes in todo.title / todo.done
// since it isn't an observer.
return <GridRow data={todo} />
// CORRECT: let `TodoView` detect relevant changes in `todo`,
// and pass plain data down.
return <GridRow data={{
title: todo.title,
done: todo.done
}} />
// CORRECT: using `toJS` works as well, but being explicit is typically better.
return <GridRow data={toJS(todo)} />
)
<Observer>
回調組件可能需要 想像一下相同的例子,其中 GridRow
接受一個 onRender
回調函數。由於 onRender
是 GridRow
渲染週期的一部分,而不是 TodoView
的渲染週期(即使它在語法上出現的位置),我們必須確保回調組件使用 observer
組件。或者,我們可以使用 <Observer />
建立一個內聯的匿名 observer
const TodoView = observer(({ todo }: { todo: Todo }) => {
// WRONG: GridRow.onRender won't pick up changes in todo.title / todo.done
// since it isn't an observer.
return <GridRow onRender={() => <td>{todo.title}</td>} />
// CORRECT: wrap the callback rendering in Observer to be able to detect changes.
return <GridRow onRender={() => <Observer>{() => <td>{todo.title}</td>}</Observer>} />
})
提示
伺服器端渲染 (SSR)
如果在伺服器端渲染環境中使用observer
;請確保調用 enableStaticRendering(true)
,這樣 observer
才不會訂閱任何使用的 observables,也不會產生 GC 問題。**注意:** mobx-react vs. mobx-react-lite
在本文件中,我們預設使用mobx-react-lite
。 mobx-react 是它的大哥,它在底層使用了 mobx-react-lite
。它提供了一些在新項目中通常不再需要的功能。mobx-react 提供的額外功能:
- 支援 React 類別組件。
Provider
和inject
。MobX 自己開發的 React.createContext 的前身,現在已經不再需要了。- Observable 特定的
propTypes
。
請注意,mobx-react
完全重新打包並重新導出 mobx-react-lite
,包括函數組件支援。如果您使用 mobx-react
,則無需將 mobx-react-lite
作為依賴項添加或從任何地方導入。
**注意:** observer
或 React.memo
?
observer
會自動套用 memo
,因此 observer
組件永遠不需要包裝在 memo
中。可以安全地將 memo
套用於 observer 組件,因為如果相關,props 內部(深層)的變異無論如何都會被 observer
拾取。**提示:** 基於類別的 React 組件的 observer
如上所述,基於類別的組件僅透過 mobx-react
支援,而不支援 mobx-react-lite
。簡而言之,您可以像包裝函數組件一樣將基於類別的組件包裝在 observer
中
import React from "React"
const TimerView = observer(
class TimerView extends React.Component {
render() {
const { timer } = this.props
return <span>Seconds passed: {timer.secondsPassed} </span>
}
}
)
查看 mobx-react 文件 以獲取更多資訊。
**提示:** 在 React DevTools 中顯示良好的組件名稱
React DevTools 使用組件的顯示名稱資訊來正確顯示組件層次結構。如果您使用
export const MyComponent = observer(props => <div>hi</div>)
則在 DevTools 中將不會顯示任何顯示名稱。
可以使用以下方法來解決此問題
使用帶有名稱的
function
而不是箭頭函數。mobx-react
會從函數名稱推斷組件名稱export const MyComponent = observer(function MyComponent(props) { return <div>hi</div> })
轉譯器(如 Babel 或 TypeScript)會從變數名稱推斷組件名稱
const _MyComponent = props => <div>hi</div> export const MyComponent = observer(_MyComponent)
再次從變數名稱推斷,使用預設導出
const MyComponent = props => <div>hi</div> export default observer(MyComponent)
【**已損壞**】明確設定
displayName
export const MyComponent = observer(props => <div>hi</div>) MyComponent.displayName = "MyComponent"
在撰寫本文時,這在 React 16 中已損壞;mobx-react
observer
使用 React.memo 並遇到此錯誤: https://github.com/facebook/react/issues/18026,但它將在 React 17 中修復。
現在您可以看到組件名稱
{🚀} **提示:** 將 observer
與其他高階組件組合時,請先套用 observer
當 observer
需要與其他裝飾器或高階組件組合時,請確保 observer
是最內層(最先套用)的裝飾器;否則它可能什麼也不做。
{🚀} **提示:** 從 props 推導 computeds
在某些情況下,您的本地 observables 的計算值可能取決於您的組件接收的一些 props。但是,React 組件接收的 props 集合本身不是 observable 的,因此 props 的更改不會反映在任何計算值中。您必須手動更新本地 observable 狀態才能從最新數據中正確推導計算值。import { observer, useLocalObservable } from "mobx-react-lite"
import { useEffect } from "react"
const TimerView = observer(({ offset = 0 }) => {
const timer = useLocalObservable(() => ({
offset, // The initial offset value
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
},
get offsetTime() {
return this.secondsPassed - this.offset // Not 'offset' from 'props'!
}
}))
useEffect(() => {
// Sync the offset from 'props' into the observable 'timer'
timer.offset = offset
}, [offset])
// Effect to set up a timer, only for demo purposes.
useEffect(() => {
const handle = setInterval(timer.increaseTimer, 1000)
return () => {
clearInterval(handle)
}
}, [])
return <span>Seconds passed: {timer.offsetTime}</span>
})
ReactDOM.render(<TimerView />, document.body)
在實務中,您很少需要這種模式,因為 return <span>經過的秒數:{timer.secondsPassed - offset}</span>
是一個更簡單的解決方案,儘管效率略低。
{🚀} **提示:** useEffect 和 observables
useEffect
可用於設置需要發生的副作用,並且這些副作用與 React 組件的生命週期綁定。使用 useEffect
需要指定依賴項。使用 MobX 並非真正需要這樣做,因為 MobX 已經有一種方法可以自動確定 effect 的依賴項,即 autorun
。幸運的是,將 autorun
與組件的生命週期結合起來並使用 useEffect
非常簡單
import { observer, useLocalObservable, useAsObservableSource } from "mobx-react-lite"
import { useState } from "react"
const TimerView = observer(() => {
const timer = useLocalObservable(() => ({
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
}
}))
// Effect that triggers upon observable changes.
useEffect(
() =>
autorun(() => {
if (timer.secondsPassed > 60) alert("Still there. It's a minute already?!!")
}),
[]
)
// Effect to set up a timer, only for demo purposes.
useEffect(() => {
const handle = setInterval(timer.increaseTimer, 1000)
return () => {
clearInterval(handle)
}
}, [])
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
請注意,我們從 effect 函數中返回由 autorun
創建的 disposer。這一點很重要,因為它確保在組件卸載後 autorun
得到清理!
依賴項數組通常可以留空,除非非 observable 值應觸發 autorun 的重新運行,在這種情況下,您需要將其添加到那裡。為了讓您的 linter 滿意,您可以將 timer
(在上面的示例中)定義為依賴項。這是安全的,並且沒有其他影響,因為引用實際上永遠不會改變。
如果您希望明確定義哪些 observables 應觸發 effect,請使用 reaction
而不是 autorun
,除此之外,模式保持相同。
如何進一步優化我的 React 組件?
查看 React 優化 {🚀} 章節。
疑難排解
救命!我的組件沒有重新渲染...
- 確保您沒有忘記
observer
(是的,這是最常見的錯誤)。 - 驗證您打算 react 的東西確實是 observable 的。如有需要,請使用
isObservable
、isObservableProp
等工具在運行時驗證這一點。 - 檢查瀏覽器中的控制台日誌是否有任何警告或錯誤。
- 確保您了解追蹤的工作原理。查看 了解反應性 章節。
- 閱讀上述常見陷阱。
- 配置 MobX 以警告您機制的不當使用並檢查控制台日誌。
- 使用 trace 驗證您是否訂閱了正確的東西,或者使用 spy / mobx-log 包檢查 MobX 的一般操作。