How to Support Multiple Windows in React Native

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

This lesson preview is part of the Building React Native Apps for Mac course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Building React Native Apps for Mac, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course Building React Native Apps for Mac

Hi, welcome to the first lesson of the advanced macOS functionality. So when you're using your desktop apps, you expect them to support multiple windows. Your browser has windows, your terminal has windows, your code editor has windows, right? You can have multiple instances of the application. So this is something that we haven't touched so far in our application. Everything has been done within a single window app. So we're gonna actually take on this challenge and that support for driving multiple windows. So we will start by abstracting our bridge, a React Native bridge. In theory, we don't need to do this. We could just create more windows instances and use a single bridge instance. But we're gonna be a little smarter about it and also because we just want to practice and understand the internal server React Native. So we're just gonna create one abstraction layer on top of that. So in order to do this, I'm going to go into Xcode and inside the macOS folder, I'm gonna create a new group, which is effectively just a folder. And I'm gonna call it lib. And inside of this, leave, I'm gonna create two files. The first file is going to be a React View Controller. And the second file is going to be a React Native bridge file. So basically what we're gonna do from a very high level perspective is we're just gonna wrap some of the code that takes to initialize a React Native view. We could avoid this again, but by having this abstracted in two classes, it just becomes a little bit easier to initialize different views and different application instances. So I'm just gonna take the code from the lesson. Just on the React View Controller. So let's just quickly walk over the code. The class is React View Controller and it extends NSViewController. This is just a macOS class. Inside of it, whenever the class gets initialized, I'm gonna create an RCT root view. If you remember, this is the React Native view that we used to mount our application. And the rest is just gonna placeholder code, right? I'm not doing anything too fancy about it. With the exception of passing a bridge instance and the module name, which are gonna get passed into the RCT root view. Next, I'm going to go into R. Give me one second. Great. So I'm going to go into... Okay, I'll save that and I'm going to go into the React Native bridge and I'm going to paste our code again from the lesson. So inside of the React Native bridge, I have a RCT bridge instance, which is gonna hold my React Native bridge. And whenever this class gets initialized, I'm just creating a new bridge. I need to pass a delegate. This is just one of the things that are needed because the class requires it. So here I have my helper class, which is the delegate. It extends NSObject and the RCT bridge delegate. The only function that I need to take care is the source URL function, right? Whenever the bridge starts, it's gonna start looking for where to load the JavaScript from. So it's gonna invoke this function. So basically it just needs to return URL. And this is pretty much the same code that we have on the app delegate, right? I just plugged it from there and put it inside this class. So with these two classes, you can see we can just initialize a view controller and internally is gonna initialize a new bridge. That means we initialize a new window, that window needs a controller, so we initialize a controller and the controller takes care of initializing a new bridge instance. So in theory, you shouldn't have a much of a bandwidth problem if you have multiple windows because each window will have its own bridge instance and that can communicate between the JavaScript and the native side. Great. So now we can go back, I'm gonna save this, and we can go back into our app delegate file. So on my app delegate file, I am going to add a couple of variables. First, I'm gonna add a reactive bridge and I'm gonna add one more window instance. I already have one window instance, so I'm gonna rename this to the dev window and all the code that I had down here, I'm just gonna replace it. Right, there's still one my dev window for my main application view and the extra window, I'm just gonna initialize afterwards. Great. So now you can see this code and this code and this code and this code, these are all the code that we have abstracted away within our two classes. We basically don't need this anymore. So I'm gonna take one more time the code from the lesson and I'm gonna replace all of this. So let's quickly go over it. First with my application finishes launching, I'm gonna create a new bridge, right? Because we said we want to reuse the bridge in our case just because. So I'm gonna create a single bridge instance to drive all my windows. Like some gonna create a popover controller, right? This is my React View Controller class, which is gonna look for my application and it's gonna use the single bridge instance that I have created before. Next I create the popover as before and I just replaced my old controller line that I had down here, I replaced it with my new popover controller. And I'm gonna reuse that controller into my dev window. Great. So let's just test this out to see how it's working. Great. So all my application compiles and it should start. Great, perfect. Right. So now we know that our abstractions work properly. Everything is working fine. And I can actually move on to actually opening a new window, a new window with a new instance of our application. So down here, I'm gonna add a new function. Just missing one parentheses. And this is just I want to open a proper desktop window because remember our application actually is running as a status bar app, right? So what I want is whenever the user clicks on a button, I want a new window to be opened. So this is the function that I'm gonna call. So basically what I'm gonna do here is gonna ask is my window instance existent , right? If it's not existent, if it doesn't exist, then I'm gonna create a new view controller. And I'm gonna reuse the bridge, right? I'm not creating multiple bridge instances, even though I could. I'm just gonna reuse that one. And the name here is gonna be slightly different, right? Because this is my window instance of my app. It might be a different application itself, right? It might be a different root container. Might have different routes. It might have different style. It doesn't matter, right? I'm just gonna register it as a complete new React Native application. In this piece of the code you should be fairly familiar with, right? I'm setting the initial dimensions, I'm placing it on the screen, I'm putting a title on it, everything is more or less the same. And if the window already exists, I don't want to create right now my app. I only wanted to have one status bar, window, the popover, and one separate window, I don't say. So I'm just gonna take that window instance, and I'm gonna tell it to be the focus window. Great, so now we have all the basic pieces. And we need to go and expose this functionality to our JavaScript type. So by now you should be quite familiar with this, right? This is just, we're adding one external method. We're gonna call this open the desktop window. I'm gonna go into my Swift file, and I'm just gonna take the code from the lesson now. And it's just a regular function. I am doing the same thing as when I'm closing the app, right? Because this is a UI operation, so it needs to go on the main thread. I'm reaching for my app delegate, and I'm just calling the open desktop window. As the last step, I'm going to go back into the JavaScript code, into my native class. And I'm just gonna add the method, open desktop window. And I'm just gonna bind that to that method, open desktop window from my native module. And that should be it. So now I need to find a way where I need to put away for the user to call this functionality. So once again, I'm just gonna place a button next to my quit button. Let's just make this a little bit prettier. Just so that our application doesn't look too bad. And instead of quit, this is going to be a desktop window button. Here I'm just gonna call my open desktop window and that's it. So let's make this a little bit prettier. Just because. If I save this and let me just pass. Good enough for now. So now I have my button and if I click on it, I'm gonna get an error because it 's not a function, right? I haven't exposed my module yet. So this is something that you might get a feeling for. Sometimes you will need to recompile your app because you have added native code. Sometimes your node cache might be incorrect, right? So you might need to restart the package here and so on and so forth. So this is my dev window. I'm just gonna close it to make it completely clear. We're on our status bar item. And now if I click this, you will see we have an error, right? Because it says this building underscore apps window, it has not been registered, right? So Metro or the new instance that we have created on our app delegate, it's trying to look for this specific package, right? For this specific react native application, which we haven't registered at all. We just have placed the button and we try to create a window. So we need to register this, this package in order for our application to work. So I'm gonna go back into my index.js file because if you remember at some point during the previous lesson, this is where we saw I had a small error where I forgot to change the name of the code that I pasted. I forgot to put the correct name in here, right? This is where I register, let us say, a whole react native application. So for now I'm just gonna do the same in here and I'm gonna register the component and I'm just gonna, for now I'm just gonna hard code this. And I'm gonna return an instance of my same root application, my same root app. tsx file. But you know, just think about it here, you can create a brand new app, right? It doesn't have to be the same one. It could be an app that has different routing, it behaves differently from the part of the application that's gonna run on the status bar, right? So here you could create, you could have several mini react native apps that run in different windows or different modes or I don't know, the world is your oyster basically. So I'm just gonna go compile my lab one more time. And let's test this, hopefully it will work. And there you go, right? This is a complete brand new app. It uses the same bridge as our status bar app, but it's independent of it. It could be completely different, it could be a settings window, it could be a different type of implementation that looks at the same data and doesn't matter. [silence]