Complete Intro to React, v8 Modals with Portals
Transcript from the "Modals with Portals" Lesson
>> So let me explain to you the dark, dark days of writing React. If I was on this website and I wanted to do a modal, which by the way, objectively models, terrible user interface. I do not like them. Please stop using them. Now that I've said that, I'm going to show you how to do a modal in React.
[00:00:21] In the dark, dark days of React, it was really hard to do. Cuz if I click adopt Luna here and I want it to pop up, are you sure you wanna adopt Luna? For many reasons, that dog is a jerk sometimes. But if you wanna do some sort of user confirmation there, think about how you would write that in React.
[00:00:41] With a modal you typically want it to be like the topmost element of an application, right, so that the CSS renders over everything, right? So that it has kind of precedents there. Well, how are we gonna do that in React, right? Just think about our tree here if we look into the dev tools, inspect, and we go to Components.
[00:01:03] That's kind of a cool trick I just showed you right there. So I can just like right click Inspect Element here, here on this particular element, and then if I just go directly to the component one here, it'll actually take me directly to that one. So it's smarter to say you're highlighting this in the normal explorer, you go to the component once it's going to highlight the same thing in the React tree.
[00:01:24] Okay, so our carousel's all the way down here, right? How am I gonna render something that's above the header? It's deeply nested in here, right? So, it gets really tricky really quickly of how to render something above something. To give you a separate example that's not a modal, imagine that I have a component here.
[00:01:50] And then maybe I have some sort of like contextual navigation above here where I show maybe the Adopt Luna button up here in the navigation. How do I render something from in carousel or in the details page to the navigation? It's possible, right, we could just have top level state in app.jsx, right?
[00:02:12] And that we could just pass that into details and we can pass it into header. And then we have to pull all of our state up into app.jsx, which kind of sucks, right? We want that state to live in details because all of that logic could live together and that would be very nice, right?
[00:02:28] So, that is how we used to have to do it until something called portals came along. Portals basically allowed you to render to a different place from within a component, right? So, actually what we're gonna do here is we're gonna go to our index.html and we're gonna put another div right here, and we're gonna call it id equals portal.
[00:02:54] And we're gonna put nothing in there for the moment. But now we have this other place in the DOM where we can render stuff into. And we can do it from within some deeply nested component, right? And that ends up being really useful sometimes. Or I called it portal here, I think in my notes I actually call it modal, so we're gonna call it modal.
[00:03:19] Whatever you wanna call it here, but for the moment let's just call it modal. Okay, then come in here and make a new file, call it modal.jsx. Okay, and here inside of modal.jsx, I want you to import use effect and use ref. We're gonna use another hook here called ref from React.
[00:03:55] And we're going to import create portal from react-dom. Okay, make a function here, call it modal it's gonna take in children. Just like Our error boundary, this is basically going to mostly seamlessly pass children through if it has children, and if it doesn't even have any children, then it's not going to render itself, Okay, we're going to use a ref here.
[00:04:38] And we're gonna say use ref, and by default it's just gonna be null. A ref is basically I have this piece of something and I need that the same thing back every single time, right? So I'm going to create a div and I wanna have the same div back every time.
[00:04:59] Whereas if I'm creating the div anew every single time, I'm getting a different div on every render, right? And I'm going to render into that div, which is going to be our modal. So bear with me for a second, I know that's a little abstract. Gonna say, if I have no elRef Dot current,then create one.
[00:05:24] elRef.current equals document.createElement div. You can think of basically an elRef or a ref as a container to give yourself back the same thing every single time. That is the point of a ref. So here in this elRef.current it's always going to be the same div that I create here and it's going to create this div once.
[00:05:53] Cuz I want the same div on every single rerender of this. And now that I have this handle on this div, I can attach it to the DOM, I can detach it from the DOM, and I'm always operating on the same div. Okay, here now I'm gonna use a use effect.
[00:06:11] I'm gonna say, const modalRoot equals document. Don't do that. Document.getElementById modal. That's the one that we just put into our index.html, right? ModalRoot.appendChild elRef.current, and then And then at the end of this we're going to. We only want this to happen once, right? We don't want it to keep appending itself over and over again.
[00:06:55] And then lastly here, we're going to return createPortal. I'm gonna wrap this in a div. You might ask, why aren't we just passing children directly through, which you totally could, right? I don't need to div here for technical reasons, right, you can just say children. But we are going to put a div here around children, because my CSS classes require it.
[00:07:19] So it's purely for styling purposes, not cuz we actually needed it. And then we're gonna pass it into elRef.current And then down here at the bottom, export default Modal. Really close, you're mad about this cuz you need an equal sign, okay. We have one problem here and it's on this line.
[00:07:55] Whenever this is finished, cuz I'm gonna render a modal, do you wanna adopt this pet? And then after it's done, they click yes or no, we need to get rid of the modal, right? It needs to go away. So we need some sort of cleanup to happen afterwards.
[00:08:10] If you're using a class component you would use component will unmount, right, cuz a component's about to exit the DOM, you wanna run that method. How do you do component will unmount in a function component? It's kinda weird shape of an API, but what you do is in this effect here, we're going to return a function that modalRoot.removeChild elRef.current.
[00:08:48] So whenever you return an effect or basically return anything, in fact, it will run this whenever the component will unmount. It's kind of weird, but this is how you do component will unmount. Anything that you return in an effect will be run after a component has been unmounted from the DOM.
[00:09:09] It's not has been, its will be, right, it does it before it totally unmounts. So here we are now removing the child at the end of it, which is important to do otherwise we're gonna get Infinite divs just being left inside of this This portal, this modal. Does that make sense?
[00:09:33] Cool. So yeah, the reason I typically end up doing something like this is either to remove something from the DOM, which we've done here. To remove event listeners, if you're doing some sort of external event listening, you'd have to go in here and you have to remove an event listener, that would happen here.
[00:09:56] If you're like stopping a timer, if you're doing something like a set timeout or a request animation frame or what's the one I'm thinking of, set interval. Yeah, set interval, right, you would cancel the interval here. Last thing I can kind of think of is, on some rare unfortunate occasion you have to have a React component wrapped like a jQuery component.
[00:10:21] Any sort of cleanup that you would have to do there, that would happen here as well. Those are kind of reasons why you would end up doing something like this. Cool. So, any questions about what we've done here in modal.jsx? I purposely structured this little bit so that it's easier to do with TypeScript later.
[00:10:47] It's like creating the modal root in here and all that kind of stuff. You absolutely could just create this once outside here like this. That absolutely would work, but it makes the TypeScript really ugly, so I just do it inside of the modal root. Cuz there's no reason to do it here as well or no reason not to do it here as well.
[00:11:12] Okay, awesome. Yeah, another question that one might ask here, why don't I just have const elRef equals useRef Like this. Why don't I do it this way? How many times iis modal gonna get run by other kinds of function? A lot, potentially. Potentially, could be run a lot.
[00:11:56] Now if I provide it here it's basically just saying this is the default and then after that you can ignore it, right? The problem is, even though I'm not using it over and over again, this document.createElement is still gonna get called and it's just gonna throw away the result.
[00:12:14] Anything that you say document dot something, it's just slow by default, and all DOM API's, you need to question how fast is this, right? So I'm typically not one for performance things like that, right, but in this particular case, churning through arbitrarily created divs that you're throwing away, not a good use of your CPU time.
[00:12:36] Whereas you can do something like this, right?
>> Could you not replace the document.getElementById modal with a useRef for it as well?
>> Could you put a ref here and have that reference the modal? You could, there's just no reason that you would need to, cuz this use effect is only gonna get run once.
[00:13:00] So I don't need to keep track of it between renders. I just need it inside of the use effect, and that's it. The only reason why I do it here is cuz I'm creating a div that eventually I'm going to remove. Why do we do it this way by the way?
[00:13:15] We could totally just reuse the same div over and over and over again. We do it this way because in theory you could have multiple modals going on, in which case I have lots of questions for you. But this makes it a little bit more reusable because it's a self contained starts up and cleans itself up kind of sort of situation here.
[00:13:38] It's not totally reusable in that aspect cuz we're always rendering into modal, so it really isn't reusable. But here the way you would solve that is you would just have props.target or something like that. And then you would have a target in here. Or yeah, just target. And then you really could have multiple modals, which good luck with your CSS.