javascripthtmlreactjsmaterial-uiproducer-consumer

How to open component's dialog from another component in react?


I have a main component, App(). I have another component, EventPage(). EventPage has a dialog, defined in a separate file, that opens and populates from a click on a table row. In that case, the data in the dialog can be modified and saved to a database.

The second case is to create a new event using the same dialog. The Add button for that is in another component, App(). When clicked, I want EventPage to open its dialog, un-populated.

I've seen many similar examples but I can't get them to work for my use case. I think I need to useContext but I don't get how to apply it. Here's some code to clarify:

index.tsx

ReactDOM.createRoot(document.getElementById("root")!).render(
  <App/>
);

App.tsx

const App = () => {    
  return (
    <Router>
          <MainAppBar isAppSidebarOpen={false} />
    </Router>
  );
};

The router goes to Event page from menu click in MainAppBar. A click on a table row opens and populates a MUI dialog. It will open just by setting the open property to true.

event.tsx

  function handleRowClick(event: React.MouseEvent<unknown>, id: number) {
    const selectedRowData = getSelectedRowData(id);
    setDialogData(selectedRowData);
    setOpen(true);
  }

Now I need to open the same dialog, unpopulated, on a button click in the App component.

App.tsx

function ClickAddIconComponent(props: { key: string }) {
    return (
      <Fab size="small" color="primary" aria-label="add" onClick={handleAddIconClick} sx={{ marginTop: "6px", textAlign: "right"}}>
        <AddIcon />
      </Fab>
    )
  }
}

function handleAddIconClick() {
  <TellEventPageToOpenDialog message="openEmptyDialog" />;
}  

I'm assuming I need to useContext() to open Event's dialog?

interface MessageContextType {
  message: string;
  setMessage: React.Dispatch<React.SetStateAction<string>>;
}

const MessageContext = React.createContext<MessageContextType | null>(null);

const TellEventPageToOpenDialog = (props:any) => {
  const [message, setMessage] = React.useState(props.message);

  return (
    <MessageContext.Provider value={{ message, setMessage }}>
      <EventPage />
    </MessageContext.Provider>
  );
}

Now how do I get EventPage to receive receive the message (from App) and open its dialog? Am I going about this wrong?

EDIT: I don't want to render the dialog from App because it doesn't have access to all the many props that come from EventsPage, including callbacks. This is the call to show the dialog from EventsPage:

  <EventDialog         
    open={open}
    dialogData={dialogData}
    slotProps={{
      paper: {
        component: 'form',
        onSubmit: (event: React.FormEvent<HTMLFormElement>) => {
          event.preventDefault();
          const formData = new FormData(event.currentTarget);
          const formJson = Object.fromEntries((formData).entries());
          saveEvent(formJson)
          addToTable(formData);
        },
      },
    }}
    isLoading={isLoading}
  />

You see that it includes many callbacks that are not available from App. That is why I need App to tell EventsPage to show the dialog.


Solution

  • I went to a producer/consumer route instead, where App is the producer and EventPage is the consumer. App produces a message when the Add button is clicked, while EventPage listens for that message and opens the dialog when it gets it. This pattern is facilitated by an EventBus, which I store in a separate file.

    event-bus.tsx

    class EventBus {
      private listeners: { [key: string]: Function[] };
    
      constructor() {
        this.listeners = {};
      }
    
      on(event, callback) {
        if (!this.listeners[event]) {
          this.listeners[event] = [];
        }
        this.listeners[event].push(callback);
      }
    
      off(event, callback) {
        if (this.listeners[event]) {
          this.listeners[event] = this.listeners[event].filter(
            (listener) => listener !== callback
          );
        }
      }
    
      emit(event, data) {
        if (this.listeners[event]) {
          this.listeners[event].forEach((listener) => listener(data));
        }
      }
    }
    
    const eventBus = new EventBus();
    export default eventBus;
    

    App.tsx

    import EventBus from './event-bus';
    
    function handleAddIconClick() {
      eventBus.emit('buttonClicked', { message: 'Show Dialog' });
    }
    

    events.tsx

    import EventBus from "./event-bus";
    
      useEffect(() => {
        eventBus.on('buttonClicked', handleIncomingMessage);
    
        return () => {
          eventBus.off('buttonClicked', handleIncomingMessage);
        };
      }, [open]);
    
      function handleIncomingMessage(data: any) {
        setDialogData(initialDialogData);  
        setOpen(true);
      }