javascriptreactjsreact-contextmobxmobx-react-lite

React - Pass JSX / React component to "unrelated" component


I'm looking for a flexible approach that will allow me to send some JSX to a totally unrelated component. For example, a modal.

Let's assume we have some boilerplate code for a global store and we can access it from any component with a custom useStore hook. So a very barebones modal could look like this:

// Modal.jsx
function Modal() {
  const store = useStore()

  if(!store.modal.isVisible) return null

  return <div>{store.modal.content}</div>
}

Now, if I just want to display some text, that works perfectly fine. And even though JSX shouldn't belong in the store, it kind of works when store.modal.content is a react component. However, it breaks when using hooks. The following would be an ideal solution syntax-wise, but doesn't work with mobx as the store. And like I said, I don't want to risk some strange behaviour by putting JSX or a useRef() reference in the store.

function ToggleThing() {
  const [isActive, setActive] = useState(false)

  return <button onClick={() => setActive(!isActive)}>{isActive.toString()}</button>
}


function SomeModalTrigger() {
  const store = useStore();

  return (
    <button
      onClick={() =>
       store.setModalContent(<ToggleThing />) // <-- This would be perfect
      }
    >
      Trigger Modal
    </button>
  );
}

Non-ideal solutions

The content needs to be truly dynamic, so a solution to just save a string and maybe some props in the store and use an object as a component lookup wouldn't work.


const modalComponents = {
  someComponent,
  anotherComponent
}
// -> modalComponents[store.modal.component]

Also, I'd like to avoid some dangling variable that stores the JSX and is not part of a store/hook/component. Even though this would work it makes maintenance difficult and breaks the core concept of react

let modalContent = null // Not in a component

function Modal() {
  const store = useStore()

  return <div>{modalContent}</div> 
  // If modalContent is updated without triggering a component rerender, the content becomes stale
}

function SomeModalTrigger() {
  const store = useStore();

  return (
    <button
      onClick={() => {
        modalContent = <ToggleThing />;
        store.triggerModalUpdate();
      }}
    >
      Trigger Modal
    </button>
  );
}

Note: The actual code is much more complex than that and not really about modals, so it's not as easy as using a modal package. Modals were just the most approachable way to describe the problem for me.


Solution

  • You're right; it's not ideal to put JSX elements into store-like places because they're not serializable.

    One way to achieve this is to use createPortal, which allows you to portal/render a JSX element (component) under a specific DOM node.

    If you only need to display one modal at a time, you can give the modal-container a unique and fixed id and retrieve the DOM node using document.getElementById in SomeModalTrigger, as demonstrated in the official documentation.

    If you need to display multiple modals at the same time, you may need to give each modal a dynamic id (which you can generate using the new useId hook), and store this id(s) in the store so that it's accessible to SomeModalTrigger.