Search code examples
reactjsmobxmobx-reactmobx-state-treemobx-react-lite

How to make child component reactive to state change in mobx using mobx-react-lite


I'm using mobx-state-tree and mobx-react-lite, can someone guide me to a better pattern,

wishlist.js - wishlist store

import { types } from 'mobx-state-tree'

export const WishListItem = types.model('WishListItem', {
  name: types.string,
  price: types.number,
  image: "",
}).actions(self => ({
  changeName(newName) {
    self.name = newName
  },
}))

export const WishList = types.model('WishList', {
  items: types.optional(types.array(WishListItem), []),
})

root.js - root store

export const RootStore = types.model('RootStore', {
  counter: types.optional(Counter, { count: 0 }),
  wishList: types.optional(WishList, {
    items: [{ image: '', price: 10, name: 'Yoda' }]
  }),
})

I'm updating the store as

setInterval(() => store.wishList.items[0].changePrice(Math.random() * 100), 500)

In my Wishlist view wishlist.jsx

const WishListItem = ({ image, name, price }) => {
  return useObserver(
    () =>
      <div>
        <img src={image} />
        <h3>{name}</h3>
        <h5>{price}</h5>
      </div>
  )
}

const WishListView = ({ items }) => {
  return useObserver(
    () => <>
      {
        items.map(
          (item, key) => <WishListItem {...item} key={key} />
        )
      }
    </>
  )
}

export default () => useObserver(() => (
  <WishListView items={store.wishList.items} />
))

Here I have to use useObserver or Observer at every level of the component tree, to make it reactive, is there any way to pass a reactive reference to the child?

It works perfectly fine with primitive types like string or number, but with an array or an object, I have to either directly refer changing variables at the parent like store.wishList[0].price or use useObserver in the whole tree.

I want to pass the items array to children, and update children on the changes, just this at the root

export default () => useObserver(() => (
  <WishListView items={store.wishList.items} />
))

and no more useObserver at it's childrens

Update

A workaround I found was to destructure the array, now the changes are reactive since we are directly accessing the variables that are changing.

export default () => useObserver(() => {
  const items = store.wishList.items.map(item => ({ ...item }))
  return <WishListView items={items} />
})

Solution

  • and no more useObserver at it's childrens

    It is actually better to mark all components as observer if possible. For example, if you mark each Item as observer and one of the items change its name then only this component will rerender. If you dont make Item observer then your whole List will rerender which is quite bad if have lots of items or deep DOM tree. Also it does not make sense to rerender whole list when just one item changes.

    Look here for explanation https://mobx.js.org/refguide/observer-component.html#when-to-apply-observer

    So your workaround is a bad pratice and should be used only as last resort if you dont have control over children components and can't make them observer.