Side effects and observables

You might have been using React.useEffect for several reasons, eg. setting document.title dynamically or for managing timers.

What if your side effect actually depends on observable value? Don't worry, MobX got you covered.

  1. import React from 'react'
  2. import { autorun } from 'mobx'
  3. function useDocumentTitle(store: TStore) {
  4. React.useEffect(
  5. () =>
  6. autorun(() => {
  7. document.title = `${store.title} - ${store.sectionName}`
  8. }),
  9. [], // note empty dependencies
  10. )
  11. }

Important realization here is that you will never need to specify dependencies for useEffect because there are simply none. Or to be more precise it does depend on store variable, but that's the same case as for React.useRef which value you shouldn't specify in dependencies also.

It surely is slightly verbose. We might eventually provide utility hooks like useAutorun or useReaction, but these are very easy to make on your own if you don't want to wait.

Since the autorun (and others from MobX family) returns a disposal function, it's ensured that useEffect will take care of disposal on unmount of the component.

Non-observable dependencies

What if effect depends on value from eg. props? One way is to specify it in dependency array of useEffect as you have been taught. However that means a reaction would be recreated and that's not very effective. In fact it wouldn't even work because reaction is not executed immediatelly by default (see fireImmediatelly option).

Much better way is to useAsObservableSource (learn more). Instead of constantly thinking what dependencies you should be providing, you simply use observables and let MobX figure out the rest. Isn't it great?

  1. import React from 'react'
  2. import { reaction } from 'mobx'
  3. import { useAsObservableSource } from 'mobx-react' // 6.x
  4. function useFetcher<T>(
  5. resource: string,
  6. baseUrl = process.env.BASE_URL,
  7. ): T | null {
  8. const store = useStore()
  9. const source = useAsObservableSource({ url: `${baseUrl}/${resource}` })
  10. const [result, setResult] = React.useState(null)
  11. const doFetch = (fetchUrl: string) => {
  12. store.setLoading(true)
  13. fetch(fetchUrl)
  14. .then(result => {
  15. setResult(result)
  16. })
  17. .catch(err => {
  18. store.setError(err)
  19. })
  20. .finally(() => {
  21. store.setLoading(false)
  22. })
  23. }
  24. React.useEffect(
  25. () => reaction(() => `${source.url}/${store.itemId}`, doFetch),
  26. [],
  27. )
  28. return result
  29. }

Non-reactive side effects

With the help of useFetcher hook we defined above we can showcase another approach for tackling side effects.

  1. const BookDetail = ({ bookId }) => {
  2. const store = useStore()
  3. const result = useFetcher<TBook>(`/books/${bookId}`)
  4. React.useEffect(() => {
  5. document.title = `${result.title} - ${store.sectionName}`
  6. })
  7. return useObserver(() => {
  8. if (store.loading) {
  9. return <Loading />
  10. }
  11. if (store.error) {
  12. return <ErrorDisplay error={store.error} />
  13. }
  14. return (
  15. <div>
  16. <h1>{result.title}</h1>
  17. </div>
  18. )
  19. })
  20. }

There are no dependencies for useEffect meaning it will run on every render. It's definitely ok in this case because useFetcher is a major cause of re-rendering of the component.

However, if the sectionName would be changing on its own, the autorun would need to be used again to capture observable nature of it.

Using observer HOC here would not solve anything because it cannot see inside the useEffect callback.