React Native 搭配 MobX 使用心得
MobX 是一款十分優秀的狀態管理庫,不但書寫簡潔還非常高效。當然這是我在使用之後才體會到的,當初試水上車的主要原因是響應式,考慮到可能會更符合 Vue 過來的思考方式。然而其實兩者除了響應式以外並沒有什麼相似之處。
在使用過程中走了不少彎路,一部分是因為當時掃兩眼文件就動手,對 MobX 機制理解得不夠;其它原因是 MobX 終究只是一個庫,會受限於 React 機制,以及與其它非 MobX 管理元件的相容問題。當中很多情況在文件已經給出了說明(這裡和這裡),我根據自己遇到的再做一番總結。
與非響應式元件相容問題
與非響應式的元件一起工作時,MobX 有時需要為它們提供一份非響應式的資料副本,以免 observable 被其它元件修改。
observable.ref
使用 React Navigation 導航時,如果要交由 MobX 管理,則需要手動配置導航狀態棧,此時用 @observable.ref
“淺觀察”可避免狀態被 React Navigation 修改時觸發 MobX 警告。
當 Navigator 接受 navigation
props 時代表導航狀態為手動管理。
import { addNavigationHelpers, StackNavigator } from 'react-navigation'
import { observable, action } from 'mobx'
import { Provider, observer } from 'mobx-react'
import AppComp from './AppComp'
const AppNavigator = StackNavigator({
App: { screen: AppComp },
// ...
}, {
initialRouteName: 'App',
headerMode: 'none'
})
@observer
export default class AppNavigation extends Component {
@observable.ref navigationState = {
index: 0,
routes : [
{ key: 'App', routeName: 'App' }
],
}
@action.bound dispatchNavigation = (action, stackNavState = true) => {
const previousNavState = stackNavState ? this.navigationState : null
this.navigationState = this.AppNavigator.router.getStateForAction(action, previousNavState)
return this.navigationState
}
render () {
return (
<Provider
dispatchNavigation={this.dispatchNavigation}
navigationState={this.navigationState}
>
<AppNavigator navigation={addNavigationHelpers({
dispatch: this.dispatchNavigation,
state: this.navigationState,
})} />
</Provider>
)
}
}
observable.shallowArray()
與 observable.shallowMap()
MobX 還提供其它方便的資料結構來存放非響應式資料。
比如使用 SectionList
的時候,我們要為其提供資料用於生成列表,由於 Native 官方的實現跟 MobX 不相容,這個資料不能是響應式的,不然 MobX 會報一堆警告。
MobX 有個 mobx.toJS()
方法可以匯出非響應式副本;如果結構不相同還可以使用 @computed
自動生成符合的資料。但這兩個方法每次新增專案都要全部遍歷一遍,可能會存在效能問題。
這時其實可以維護一個 observable.shallowArray
,裡面只放 key
資料,只用於生成列表(像骨架一樣)。傳給 SectionList
的 sections
props 時 slice
陣列複製副本(shallowArray 裡的資料非響應式,所以只需淺複製,複雜度遠小於上面兩種方式)。
然後 store 維護一個 observable.map
來存放每個項的資料,在項(item)元件中 inject
store 進去,再利用 key
從 map 中獲取資料來填充。
通過 shallowArray 可以讓 MobX 識別列表長度變化自動更新列表,利用 map 維護項資料可以使每個項保持響應式卻互不影響,對長列表優化效果很明顯。
// store comp
class MyStore {
@observable sections = observable.shallowArray()
@observable itemData = observable.map()
@action.bound appendSection (section) {
const data = []
section.items.forEach(action(item => {
this.itemData.set(item.id, item)
data.push({key: item.id})
}))
this.sections.push({
key: section.id,
data
})
}
}
// MyList comp
import { SectionList } from 'react-native'
@inject('myStore')
@observer
class MyList extends React.Component {
_renderItem = ({item}) => <SectionItem id={item.key} />
render () {
return (
<SectionList
getItemLayout={this._getItemLayout}
sections={this.props.myStore.sections.slice()}
renderSectionHeader={this._renderSectionHeader}
renderItem={this._renderItem}
/>
)
}
}
// SectionItem comp
@inject('myStore')
@observer
class SectionItem extends React.Component {
render () {
const {myStore, id} = this.props
const itemData = myStore.itemData.get(id)
return (
<Text>{itemData.title}</Text>
)
}
}
computed
利用 @computed
快取資料可以做一些優化。
比如有一個響應式的陣列 arr
,一個元件要根據 arr
是否為空更新。如果直接訪問 arr.length
,那麼只要陣列長度發生變化,這個元件都要 render 一遍。
此時利用 computed 生成,元件只需要判斷 isArrEmpty
就可以減少不必要的更新:
@computed get isArrEmpty () {
return this.arr.length <= 0
}
observable.map
因 JS 機制 MobX 不能檢測屬性的增刪,所以最好用 observable.map
取代簡單 {}
物件。另外 MobX 沒有提供 Set 支援,可以用 key 和 value 一樣的 Map 代替。
避免在父元件中訪問子元件的屬性
這條規則在文件也提到,原因很簡單,MobX 對於一個 observer
元件,是通過訪問屬性來記錄依賴的。所以哪怕父元件裡沒有用到這個屬性,只是為了作為 props 傳給子元件,MobX 還是算它依賴了這個屬性,於是會產生不必要的更新。最好的方式是將資料統一放在 store 中,子元件通過 inject
store 方式獲取資料。
小元件
由於 React 的機制,MobX 只能在元件層面發光發熱,對於元件內部就無能為力了。所以大元件用 MobX 很容易卡死(用其它也會),小元件才能真正發揮 MobX 自動管理更新的優勢。