I need to implement a reusable modal. I have basically two implementations in my mind.
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>
);
}
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,
Regarding you 3 questions:
The react way is definitively the way without useImperativeHandle. React code should be declarative, not imperative.
I don't know about performance. I would not expect a huge difference.
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.
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.
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, ...),