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.
CodePudding user response:
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
.
CodePudding user response:
Not sure about stores but you can use the context api. Take a look below.
Note that we wrap the component in a anonymous function even though it itself is a function. That's because useState and setState allow us to pass a callback that takes the previous state. setCounter(prev => prev 1)
So when it finds a function it assumes it's that callback and invokes it. So to avoid it calling our createElement functions, we pass our own callback that returns the component.
const { useContext, useState, createContext } = React
const ModalContext = createContext();
// Sample modal bodies
const H1 = ({name}) => <h1>{name}</h1>
const H2 = ({name}) => <h2>{name}</h2>
const App = () => {
const [Modal, setModal] = useState(() => H1);
const [modalProps, setModalProps] = useState({name: 'Foo'});
const modalContext = {Modal, setModal, modalProps, setModalProps}
return (
<div>
<ModalContext.Provider value={modalContext}>
<Page />
</ModalContext.Provider>
</div>
)
}
const ModalWrapper = () => {
const {Modal, modalProps} = useContext(ModalContext)
return (
<div>
<Modal {...modalProps} />
</div>
)
}
const Page = () => {
const {setModal, setModalProps} = useContext(ModalContext);
function setH1() {
setModal(() => H1);
setModalProps({name: 'Foo'})
}
function setH2() {
setModal(() => H2)
setModalProps({name: 'Bar'})
}
return (
<div>
<button onClick={setH1}>Set H1</button>
<button onClick={setH2}>Set H2</button>
<ModalWrapper />
</div>
)
}
ReactDOM.render(<App />, app)
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="app"></div>