Search code examples
javascriptreactjstypescriptmobxmobx-react-lite

Why Mobx observer is not triggering re-render on React recurrent component?


I am trying to create a recursive component that stores it's state in a mobx observable object.

For some peculiar reason, if the observable links another observable in it's properties, the observer hoc from mobx-react-lite does not trigger re-render, when the observable state changes.

Here is the example, where the elements, that are more than 1 deep, are not re-rendered, when the observable state changes.

...
const NodeView = (props: { node: Node }) => {
  useEffect(() => {
    autorun(() => {
      console.log(`(autorun) ${props.node.label} is expanded: ${props.node.expanded}`);
      // WRONG: when the node is a child of another node, this runs, but the NodeView is not re-rendered
    });
  }, []);

  console.log(`rerendering ${props.node.label} `);

  return (
    <>
      <div
        className="nodeHeader"
        onClick={action(() => {
          props.node.expanded = !props.node.expanded;
        })}
      >
        {props.node.label}
      </div>
      {props.node.expanded && (
        <div className="row">
          <div className="offset" />
          <div className="column">
            {props.node.children.map((child, index) => (
              <NodeView key={index} node={child} />
            ))}
          </div>
        </div>
      )}
    </>
  );
};
export default memo(observer(NodeView));

Here is how observable objects are created:

...
interface Node {
  label: string;
  expanded: boolean;
  children: Node[];
  nonObservedStuff: any;
}

const turnIntoObservable = (node: Node) => {
  const children = node.children.map((child) => turnIntoObservable(child));

  let observableNode: Node = { ...node, children };

  // i'm trying to make observable not all, but only some specific properties
  observableNode = makeObservable(observableNode, {
    expanded: true,
    children: observable.shallow
  });

  return observableNode;
};

let rootNodes: Node[] = ...

rootNodes = rootNodes.map((node) => turnIntoObservable(node));

Here is codesandbox demo

Why changing state of an observable, that is linked by another observable in it's properties, does not cause to re-render of a component, that observes that observable?


Solution

  • You are exporting observer component as a default one (export default memo(observer(NodeView));), but inside NodeView itself you are using non-observer NodeView

    You need to wrap it with observer right away, so recursive version will be reactive too:

    const NodeView = observer((props: { node: Node }) => {}
    

    Also, memo is applied automatically for all observer components, you don't need to manually reapply it.