Search code examples
javascriptreactjsmobxreact-typescriptmobx-react

Why mobX action doesn't trigger rerendering of React component


I have a weird behavior of MobX.

Here is my store in the file store.tsx. You can find it also as a sandbox.

import { action, makeObservable, observable} from "mobx";

export default class ViewStore {
  currentProjectId: string | undefined = undefined;

  constructor() {
    makeObservable(this, {
      currentProjectId: observable,
      setCurrentProjectId: action,
    });
  }

  setCurrentProjectId = (projectId: string | undefined) => {
    this.currentProjectId = projectId;
  };
}

And here is my component

import React from "react";
import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom";

import ViewStore from "./store";

interface Props {
  localStore: ViewStore;
}

interface UrlParams {
  projectId: string;
}

function AppManageProject({ localStore }: Props) {
  const urlParams = useParams<UrlParams>();

  if (localStore.currentProjectId !== urlParams.projectId) {
    localStore.setCurrentProjectId(urlParams.projectId);
    return <></>;
  }

  return (
        <div>{localStore.currentProjectId}</div>
  );
}

export default observer(AppManageProject);

The page is part of a larger app and it's reached from the route /<project id>. When you first browse to e.g. /prj1, the if block is executed, so the observable currentProjectId is set to "prj1" via the MobX action setCurrentProjectId. I expect the change of the observable to trigger a rerendering, where now urlParams.projectId === localStore.currentProjectId. But the component is actually not rerendered.

In the sandbox, from the printout in the console, you can se that you do enter in the if block, but the rerendering is not triggered.

Notice that if instead of calling the action setCurrentProjectId directly, I use a callback, e.g. setTimeout(() => localStore.setCurrentProjectId(urlParams.projectId), 0) then everything works.

Thinking that the callback works because it uses an arrow function, I also tried to replace

localStore.setCurrentProjectId(urlParams.projectId)

with a call to an arrow function:

( () => localStore.setCurrentProjectId(urlParams.projectId) ) ()

But this also doesn't work.

Does anyone have any explanation/solution of what is going on? Thank you


Solution

  • For some reason MobX does not rerender components if you update some observable right inside of the render function. I don't know exactly why, but side effects inside render function is a bad pattern anyway, so it's kinda makes sense that it does not work.

    What you can do to make you code more idiomatic is to use useEffect hook for side effects, like that:

    function AppManageProject({ localStore }: Props) {
      const urlParams = useParams<UrlParams>();
    
      useEffect(() => {
        localStore.setCurrentProjectId(urlParams.projectId);
      }, [urlParams.projectId]);
    
      if (localStore.currentProjectId !== urlParams.projectId) {
        return <></>;
      }
    
      return <div>{localStore.currentProjectId}</div>;
    }