Search code examples
reactjspopupantdmobx

MobX changing but not rerendering components


I would like to open and close an antd Modal component using a MobX store. I have the following code Here's a link to codesandbox https://codesandbox.io/s/nifty-dijkstra-g0kzs6?file=/src/App.js

import AppNavigation from "./components/Menu";
import ContactPopUp from "./components/Contact";
export default function App() {
  return (
    <div className="App">
      <AppNavigation />
      <ContactPopUp />
    </div>
  );
}

File with the MobXstore

import { createContext, useContext } from "react";
import AppStore from "./appStore";

interface Store {
  appStore: AppStore;
}

export const store: Store = {
  appStore: new AppStore()
};

export const StoreContext = createContext(store);

export function useStore() {
  return useContext(StoreContext);
}

Separate file where I declare the store

import { makeAutoObservable } from "mobx";

export default class AppStore {
  contactFormOpen = false;

  constructor() {
    makeAutoObservable(this);
  }

  setContactFormOpen = (isOpen: boolean) => {
    console.log("Changed contact form to ", isOpen);
    this.contactFormOpen = isOpen;
  };
}

The Menu.tsx

import React from "react";
import { Menu, MenuProps } from "antd";
import { useStore } from "../store/store";

const AppNavigation = () => {
  const { appStore } = useStore();
  const menuItems: MenuProps["items"] = [
    {
      label: <a onClick={(e) => handleOpenContactForm(e)}>Contact</a>,
      key: "Contact"
    }
  ];

  const handleOpenContactForm = (e: any) => {
    e.preventDefault();
    e.stopPropagation();
    appStore.setContactFormOpen(true);
    console.log("Open contact pop up", appStore.contactFormOpen);
  };

  return (
    <Menu
      items={menuItems}
      theme="dark"
      overflowedIndicator={""}
      className="header__menu award-menu header__menu--md"
    />
  );
};

export default AppNavigation;

ContactPopUp.tsx

import { Modal } from "antd";
import React, { useEffect, useState } from "react";
import { useStore } from "../store/store";

const ContactPopUp = () => {
  const { appStore } = useStore();
  const [visible, setVisible] = useState(appStore.contactFormOpen);

  useEffect(() => {
    setVisible(appStore.contactFormOpen);
  }, [appStore.contactFormOpen]);

  const handleCancel = () => {
    appStore.setContactFormOpen(false);
    console.log("Close contact from", appStore.contactFormOpen);
  };

  return (
    <Modal title="Contact us" visible={visible} onCancel={handleCancel}>
      <h2>Modal Open</h2>
    </Modal>
  );
}; 

export default ContactPopUp;

The mobx contactFormOpen clearly changes but the modal state does not. I really don't understand why... UseEffect also doesn't trigger a re render.


Solution

  • You just forgot most crucial part - every component that uses any observable value needs to be wrapped with observer decorator! Like that:

    const ContactPopUp = () => {
      const { appStore } = useStore();
    
      const handleCancel = () => {
        appStore.setContactFormOpen(false);
        console.log('Close contact from', appStore.contactFormOpen);
      };
    
      return (
        <Modal
          title="Contact us"
          visible={appStore.contactFormOpen}
          onCancel={handleCancel}
        >
          <h2>Modal Open</h2>
        </Modal>
      );
    };
    
    // Here I've added `observer` decorator/HOC
    export default observer(ContactPopUp);
    

    And you don't need useEffect or anything like that now.

    Codesandbox