Home > database >  React - Pass JSX / React component to "unrelated" component
React - Pass JSX / React component to "unrelated" component

Time:12-24

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>

  • Related