Remix entered the stage with a unique and refreshing take on how to develop on the web. But how do you integrate it into your existing ecosystem of applications? Do you want to test-drive Remix on a small project, or do you want to go full-in, but it is tricky to do a big-bang migration from your existing React app? In this talk, we're going to explore how a monorepo-based code organization can help integrate Remix with your existing React and TypeScript infrastructure, facilitating high code reuse and a migration path to Remix.
Remixing Your Stack in a Monorepo Workspace
AI Generated Video Summary
Let's talk about remixing our stack in a Monorepo workspace, which allows for incremental migration and is suitable for transitioning from a Next.js app to a remix stack. Refactoring may be required for feature-specific and Next.js-coupled components, but the process is simplified because the features have already been moved out. Configuring the Monorepo to reference packages locally and linking them to the Next.js application is necessary. Nx provides benefits like fast refreshing, pre-configured setups, and features like local and remote caching.
1. Remixing Stack in Monorepo Workspace
Let's talk about remixing our stack in a Monorepo workspace. Your routing files should configure the routing for your specific stack, while the implementation of the feature should live elsewhere. You can organize your code by having a routing folder and a features folder inside your application. Alternatively, you can move the features into a dedicated packages folder, resulting in a clean and encapsulated structure. This setup allows for incremental migration and is suitable for transitioning from a Next.js app to a remix stack.
How to Remix Your Stack with ReactJS
Hey, let's talk about how we can remix our stack in a Monorepo workspace. And I would like to start with a quote, or actually a tweet from Ryan Florens from a couple of weeks ago, where he actually made a quite powerful statement and mentions that your routing files should actually just configure the routing for your specific stack. The actual implementation of that feature that's being visualized there, should live somewhere else, right? So, you import it, but then specifically depending on the stack you have, you configure the routing and you import it, and then it gets bundled and visualized on the webpage.
And this is a very powerful mechanism for architecting your system, but also for organizing your code. And we can take this even further. So normally, we see exactly this type of structure, what Ryan Florence mentioned. So you have kind of your application, you have the routing folder, which can be pages, routes, app, whatever. And then you should have, as Ryan mentions, like a different folder potentially inside that app, which is like your features. And then you have like the different folders for all those features. And the routing simply imports from them.
Now we could actually go a step further and even move them out of the application into dedicated packages folder. So this new structure, what you see now is we have an apps folder and now we have a packages folder. And in the apps folder, we still have our application with its routing mechanism. And then in the packages folder, we now have different folders for all of our features. And all of these features are now nicely kind of independent encapsulated, and they have a very clean API point where there is an index.ts file which exposes everything that can be used outside and everything else kind of remains inside that package. So you can imagine we get a very clean structure having such a setup here. Obviously, we can go ahead and just import as before. Our import might even look like as if we consumed it from some external package like some MPM registry. But it actually is just consumed locally in this case. But you can see it here from such an import statement.
So how does this help, actually? Well, potentially with something like incremental migration. Now, you might have noticed that in my example, I used a Next.js app, right? There's that pages folder. Well, if you use the new features or opt into new features, you might have a different routing folder there. But this was not by accident, but it was intentional. So you might nowadays have like a Next.js application with some already built in features, with some like an application that has been developed already for a couple of months or even years. And you might want to kind of slowly move over to a remix stack because for whatever reason, that is more suitable for you. So this is exactly what you could achieve in such a setup, because as you can see, our actual Next.js app and the actual feature folders are kind of already separate and nicely. And if you pay attention, this is actually already a monorepo. If you want, right. We have just a single app in there, but we have an app.
2. Adding Applications and Refactoring Features
We can add another application to the monorepo and generate a new remix app. This allows us to migrate new features or keep both apps depending on the use case. Refactoring may be required for feature-specific and Next.js-coupled components. The process is simplified because the features have already been moved out. Juergen Stromflohner, a web technologies expert, demonstrates how this works using Annex, an extensible build system. The Next app with the features folder is organized, and refactoring involves creating folders, moving the app, and making the features autonomous with their own package.
We have a couple of packages. So this is totally a monorepo, a very simple one. So what we could do is we could just add another application in that case. For instance, add a new remix app, which is our target migration destination. Right. So where we want to move over new features or where we even want to keep them both depending on the use case or deployment strategy.
So we could just generate a new remix app with the create remix command and then just add that app inside our monorepo workspace and import the features that we already have. There might be some various feature specific things or even things that are coupled to the Next.js application, obviously. So those need or might need some refactoring. But it's much easier because there have already been moved out.
So how does this work in practice? Let me show you. But first, my name is Juergen Stromflohner. I'm a good developer expert in web technologies. I'm also an ACAD instructor. And I work as the director of developer experience for a tool called Annex. And Annex is a smart, fast, extensible build system that happens to work really well for monorepo setups and application structures that we have just discussed. So let's have a look.
So I have here my Next app with that features folder, which gets imported directly from our routing configuration. And those features are actually nicely organized. So there is an index.js file which exports our functionality, which is super simple. And then we have also the design system which exports here a single button. But this is organized in the same fashion. So how would we refactor this to get such a monorepo workspace with a more modular structure? Well, first of all, we would probably go ahead and create the folders. And by the way, these are fully up to you, however you name this. The normal naming is what you find out there is like apps and packages or Apps and Lips. But it's up to you how you want to choose those. And we would probably go ahead and move our next app in this case into this app folder. As the next step, we need to factor out these features and move them to their packages folder. So they are completely independent. And as a result, we should also now make all of these autonomous in a sense that they have their own package.
3. Configuring Monorepo and Linking Packages
Let's configure the Monorepo to reference packages locally. Install the dependencies at the root of the package and link them to the Next.js application. Reference the dependencies in the Next.js app's package.json. Build the packages to generate the necessary output files. Run the Next.js application.
Their own dependencies, their own build scripts and things like that. So let me set that up for you quickly. So here we go.
We now have a source folder where the files live. We have a nearest build config here to our simple one. Each of these packages has now its package. JSON with the name build script in this case, and its own dev dependencies. And the very same setup holds for that design system package as well.
Now, the next step is we need to actually configure that Monorepo to tell NPM that whenever we reference a package, it should reference it locally rather than globally. So to do that, we need a root level package JSON here. So that package JSON is the configuration of our Monorepo. So there is most importantly that workspace property, which defines what our local folders are, where packages should be linked between each other. It might also have some global dev dependencies, as well as dependencies that all of the packages in the workspace should share among them.
But as a next step, we can go ahead and install all those dependencies at the very root of the package. So with this, we should now be able to actually link here our packages to our next JS application, because obviously our previous import, as you have seen here from the local features folder, doesn't work anymore because those features don't exist anymore. Rather, we should now go ahead and actually link them to where they are now moved. And so in this case, I would go here for the authentication feature and import it from here, my org auth, as well as here for this one, I would have here, my org design system.
But we also need to reference those dependencies in our next JS app. So we would go here in the package json of our next JS app in the dependencies folder, and reference here my org design system. And we can reference it with a star because we're not really interested in a specific version, although we could, but we rather want to consume the latest version that lives locally here in our workspace. And we do the very same for our auth part.
4. Issues with Setup and Dependencies
In the setup, there are some issues in terms of the DX. We need to ensure there is a build for the auth and design system packages. If we change something in those packages, we need a watching process to refresh the application.
It would totally work. You can see here, we get the name printed out as well as our small button visualized on the web page. So you might have noticed that there are some things in the set up which are not ideal, especially in terms of the DX. Because for instance, whenever we run our next JS application, we need to make always sure that there is a build already run for our auth and design system packages. So we need to have that dist folder already there because our next app depends on those build outputs. And similarly, also, if we change something in those packages folders, you need to be some watching process that kind of refresh the actual application.
5. Adding an X to the Monorepo Workspace
Adding an X to the monorepo workspace is straightforward. Run the npx nx at latest command and the init script to add the nx package. Configure the scripts that need to be run in order, such as the build script. Nx has a caching feature that allows selecting cacheable scripts. The setup includes a new NxJSON file with cacheable operations and a target default. Running the build command for a package or application will build its children first and then itself. The Nx commands can be used for more convenience. Rerunning the build command will use the cache. The setup can be configured to run in development mode.
Now, adding an X for instance, can help with some of these things. So let's do that. Adding an X is pretty straightforward. So you run the npx nx at latest command and you run an init script. And what this will do is it will add the nx package to this monorepo workspace. And then we'll ask you already a couple of questions to configure it. First of all, the first question is exactly what I mentioned before. So is there any, are there any scripts that need to be run in order? And our build script, as we have just mentioned, definitely needs to be run in order because we need to run the dependencies first. Similarly, an X has a caching feature. And so it asks like which of those scripts are cacheable? So def and start, probably not because that's really just about the development, but build, def and is potentially, if we would have test or linting scripts as well, those would be too. So let's select build here. We could even customize the output folder if there is any, but most commonly this build, publish, public are already covered and then also if we want to enable distributed caching. So we can definitely opt in here and install that. So now what we got here in this setup is we got a new NxJSON. So first of all, there's a new dependency with an X here in our package, JSON at the very root. We have also this NxCloud package because we opted in into the distributed caching. And the more interesting part here is that NxJSON file, which has the cacheable operations defined and the target default as well. So this is exactly the type of operation, which I mentioned before, which makes sure that whenever we run the build of some package or application, it runs the build of all its children first, and then the build itself. So let's try this out immediately. If we, for instance, remove here auth and design system, the dist folders. And then we run the build of my next app. You can now also use the Nx commands, which are more handy because we can just run Nx build and then here choose my org my next app. And so you can see now it runs two dependent project builds, which are exactly our dependencies that we have. And then it runs the actual application build of our next Jazz app. And the cool part is like, if we rerun this now it is cached. So it would immediately be split out and we can even configure this setup. So we can go ahead and say we don't just want to run this for actual build command, but also if we run in development mode for our setup.
6. Leveraging the Monorepo Setup
So now that we have this workspace, this monorepo setup configured, how can we now leverage this migration part? Well, let's add a remix app. So all I'm going to do is just cd into that apps folder and then run the create remix app. So I call it my remix app. I just want to have the basics, remix app server. I want to have typescript, and then I can just add it to my workspace. If I here navigate out to the root of my package and run npm again, it would set up and recognize now also my remix application, which lives alongside my Next app.
And so now that we have this setup, we can actually go ahead and just link to packages just in the same way we did in our Next.js app. So I can go ahead and here, take these two packages, go to the remix application, add those as well here into my system, and now I could go and import them from the remix routes. So now we have our buttons here imported. We can just go ahead and say here, welcome to remix. And that's here. Let's say our button just below it. Let's install the dependencies such that like npm can link everything together. And now we can run our remix application. If we first, again, pre-build our two dependent projects like the auth and design system library, but now if we go to localhost 3000, we have our remix app running and we also have the button included.
7. Automatic Refresh in Auth Folder
In this setup, the application automatically refreshes whenever a change is made in the auth folder. No additional setup or scripts are required.
The experience here is very nice because what we can do here, for instance, if I return the name, I can go here and I can reference that. And so when I run my application, it automatically would refresh the application whenever I change something in that auth folder. So I can go ahead and just serve here my next app. And so if I navigate to 4200, you see here my next app being run. I see here John Doe. And if I go back to my auth application on my auth library and add a couple of exclamation marks, you go back, you see it is automatically refreshed. So that is already wired up for you. You don't have to kind of set up any prebuilt steps or any watcher scripts, but this works out of the box.
8. Adding Remix App and Integrated Setup
To add a remix app, install the remix package and generate a new remix application. Reference and import things the same way as in the Next.js application. Run the remix app, and it will load up, show John Doe, and work with live refresh. The integrated setup pre-configures the stack and provides generators for remix actions, loaders, and metadata.
So what if I want to add now a remix app to this? Well, I can just go ahead and install the remix package, the remix plug-in here. And so once I have the plug-in installed, I can go ahead and generate a new remix application, so I can again run NX generate application. Now I should see the Now Remix app. I give it the name, I can give it a couple of options if they are available, and then I generate the remix app inside my app folder as we did before.
And so now that I have my remix app set up here, I can just go ahead and actually also reference and import things the same way as I did in my Next.js application. So I go back to my remix app inside the routes folder. I import auth package and just use it the very same way. Let's actually run the remix app. So then if we load the remix app, we can see it loads up, it shows John Doe, and it also works with live refresh. So if I go back to my auth part here, I remove the exclamation marks again, I go back, you can see it already refreshed and visualized the result for me. And so this is really just the tip of the iceberg here. The main advantage of such an integrated setup is that it pre-configures the stack for you. So you can actually focus on structure in your workspace, but you don't have to worry about how to build those, where to pre-build those and moreover, it comes with facilities such as those generators, which we have seen, which can do even more, such as here generating remix actions, loaders, metadata for your routes and much more.
9. Benefits of Modular Structure and Nx
In a monorepo workspace, applications become thin and handle bundling, while packages are encapsulated with well-defined boundaries. This allows for easy portability and scalability with teams working on specific packages. The modular structure enables isolated testing and diversifying the stack with different tech stacks. Nx helps manage and scale the setup, offering benefits like fast refreshing and pre-configured setups. Nx also provides features like local and remote caching and a dedicated Visual Studio code extension.
So what's more, this is really a different approach to how you organize and structure your code in such a monorepo workspace. Because what happens here is that your applications at the very top become very, very thin and they are really just the bundling part of your tag-specific setup. So this is where you include the components into your routing, how that specific tag like the next.js or remix setup demands for it, while the packages down there are more encapsulated, they are well-defined, they have well-defined API boundaries usually, and they can even be tag-agnostic.
So they don't have to, because some of those might be feature-specific things where we even expose like remix loaders, which you include in a remix application. But things like the authentication workflows can totally be tag-agnostic and be written in pure TypeScript. And so as you can imagine, this allows for much easier portability of features. And even though if you have to refactor things, it's much easier if they have already been extracted.
What you also get most interestingly is that you can run for instance the visualization of a graph. So this is specific to Nx, where you can run a Nx graph and it will show you now the structure of your application. Now in our super simple setup, we really just have two apps which depend on both labs. But in a more evolved setup, this might get more complicated. And most importantly, also this type of setup scales with your team. It is very easy now to for instance allocate teams to those specific packages where one works on the authentication part, there's another set of people and developers that work on the design system part, that are more shared across different apps that you might have. But then there's also like features such as a product list, where another team works on that is just focusing on the business aspect of the application. And also now that you have those more fine-grained packages, you can even run the tests in an isolated mode against just the product list, just the authentication part. So it's even easier to figure out like what is going wrong if tests fail because they are very easily identifiable. And moreover, if you now run this even on CI, given that graph that is now present, it's impossible to just run things that got touched. For instance, let's say we touch a single node in that graph, then only those would be actually tested and built and linked in your CI system. At the same time, this is also a way for diversifying your stack. Because as we have seen, we start from a Next.js application and have the intention of adding a remix app and then migrating pieces over. And you might even have multiple applications, multiple different text stacks at the same time, which you deploy to different type of destinations of your organization. And this is all thanks to that modular structure where the apps are decoupled from the actual modular packages. So we in particular have also seen today how we can implement such a stack with Nx and how that can help.
Now, we have seen two different approaches of how to use Nx in this setup. The first one is probably more ideal if you have an existing stack already. So let's say you have that npm workspace already in such a configuration. But now you want to add Remix to it, and you want to have Nx to help you manage that and scale that as it grows. If you start new, you probably lean towards the more integrated type of configuration, which gives a lot of more benefits, such as all that fast refreshing. The watching already works. But at the same time, it gives you that pre-configured setup where you don't have to worry about all the lower level details on your own. The cool part here is really that Nx helps you not only scale as you grow with features such as local and remote caching, but also comes with some nice DX aspects, such as dedicated Visual Studio code extension that can help you develop it in the long run. So if this sounds interesting, definitely join us and follow us on Nx Dev Tools, on Nx.dev, which is the main website, or also chime in to our community Slack, which you can find on the slide here.