简体   繁体   中英

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.

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 .

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>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM