Handling a modal visibility with parent state vs handing it with imperativeHandle and modal state

Viewed 25

I need to implement a reusable modal. I have basically two implementations in my mind.

  1. Handle the modal visibility in the parent state. Then I'll have to pass the state setter into the modal component as I'll always have a button inside the modal to close the modal.

const Modal = (props) => {
  return (
    <div>
      <button onClick={() => props.setVisibility(false)}>Cancel</button>
    </div>
  );
};

export default function App() {
  const [showModal, setShowModal] = useState(false);
  return (
    <div className="App">
      <button onClick={() => setShowModal((p) => !p)}>Toggle Modal</button>
      {showModal && <Modal setShowModal={setShowModal} />}
    </div>
  );
}
  1. Handle the modal visibility in the modal component itself, but expose a function to show/hide the modal using an imperativeHandle.

const Modal = React.forwardRef((props, ref) => {
  const [showModal, setShowModal] = useState(false);
  useImperativeHandle(ref, () => ({
    hide: () => setShowModal(false),
    show: () => setShowModal(true),
    toggle: () => setShowModal((p) => !p)
  }));
  if (!showModal) return null;
  return (
    <div>
      <button onClick={() => setShowModal(false)}>Cancel</button>
    </div>
  );
});

export default function App() {
  const modal = useRef();
  return (
    <div className="App">
      <button onClick={() => modal.current.toggle()}>Toggle Modal</button>
      <Modal ref={modal} />
    </div>
  );
}

I prefer the second one, as I think it looks cleaner, without a bunch of states polluting the parent component(there will be multiple modals on one page). The toggle function is only there for the example, I'll only ever need the show and hide functions. Plus, the visibility of the modal is a property of the modal, and I feel like the modal should be the component that is handling it. But I've heard people and even the react docs saying

imperative code using refs should be avoided in most cases

So, even though I think this is an acceptable use of refs, that's what's holding me back. I want to know,

  • which of the two is the react way of doing this
  • are there any differences in performance between the two ways
  • are there possibilities of bugs occurring if I decide to use refs
1 Answers

Regarding you 3 questions:

  1. The react way is definitively the way without useImperativeHandle. React code should be declarative, not imperative.

  2. I don't know about performance. I would not expect a huge difference.

  3. The useImperativeHandle will technically work fine, there will be no immediate bugs. But the idea behind the react way is to keep the code maintainable, easy to understand, and avoid future bugs and confusion.

Examples for problems with the imperative way:

Imagine you suddenly need to open / close a modal from multiple components. If the state is in the parent, this change is simple, you can easily pass the state and the setState callback where ever you want.

If you use useImperativeHandle you need to make sure the state is consistent, which in essence means handling the state in some parent component anyway.

Or imagine you decide to (or need to) refactor to use a reducer pattern, and dispatch a openModal action instead of calling the show "method" of your component. Again, that is easy if you are only handling states and callbacks anyway.

How to follow the declarative way, and why:

I totally understand your desire to keep the state inside the modal. I feel the same (and would be happy to be corrected here). But I'm also happy time and again that I didn't couple some state with other code, when I need to make "a quick change".

If you have multiple states and don't want to clutter your main component, you can use a custom hook, as you probably know.

And that's the beauty of this declarative style:

You don't have to think too much about that custom hook for now, you can just move the states in there as they are.

If you later feel that you should rename something or organize the state differently (but still following the react way), it is easy to throw states around between components (or hooks, or redux, ...),

  • always following the same patterns,
  • thinking only about what you are doing right now, moving states, without thinking of the logic they are related to,
  • without getting confused too much.
Related