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>
);
}
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.
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
.