React integration(在React中集成MobX)

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)

  1. 通過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)
  1. 通過全局變量
    如何獲得可觀察對象的引用并不重要,因此我們可以直接使用外部作用域中的可觀察對象
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。

  1. 使用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
)

請注意,我們不建議手動更新Providervalue。或者說使用MobX時不需要這樣做,因?yàn)楣蚕淼目捎^察對象可以自己更新。

在observer組件中使用內(nèi)部狀態(tài)

observer使用的可觀察對象也可以是內(nèi)部狀態(tài)。同樣,我們可以有幾種不同的選擇。

  1. 以可觀察類的方式使用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)槟M3衷摻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>} />
})
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容