Sharing UI code by exporting TypeScript
This lesson preview is part of the Bundling and Automation in Monorepos 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.
Get unlimited access to Bundling and Automation in Monorepos, plus 90+ \newline books, guides and courses with the \newline Pro subscription.

[00:00 - 00:11] In this lesson, we're going to start writing shared code. I've prepared a small document, so the goal is to be able to write TypeScript code that's shared within our monorepo.
[00:12 - 00:30] And basically what we care about is when we have any common code that shared between multiple apps in our monorepo, we just want to write it and support it once. We want to be able to write our internal packages in TypeScript.
[00:31 - 00:48] We must also make sure that the developer experience is good. This means that editing shared code should feel as seamless as editing application code, which means changes to shared code should trigger live reloads during development.
[00:49 - 00:58] We also must be mindful of performance trade offs. We must, as much as possible, avoid unnecessary builds or file watching.
[00:59 - 01:07] And we want to keep things fast for both development and production builds. There are two strategies that we will explore in this course to how to do that.
[01:08 - 01:27] And today we're going to go over the first one which is directly exporting TypeScript. In the previous lessons, we went over how one would be building a package, how we would create exports that support module systems or environments.
[01:28 - 01:37] But there is a shortcut that we can take. Instead of doing that, we can let the consumer handle the TypeScript and we just export TypeScript.
[01:38 - 01:46] There are obvious trade offs to this. For example, with this approach we don't generate .d.ts files of our code.
[01:47 - 01:59] This is meaningful because .d.ts files are much faster to parse. So if we had a package that had a lot of complex types, this approach would be slightly slower for development builds.
[02:00 - 02:10] However, the trade off is that this is incredibly fast and simple. We are, after all, just exporting the raw TypeScript without doing anything extra.
[02:11 - 02:30] There is a second approach where we can build code separately for our internal packages and then orchestrate compilation and live development, but that's more complex and we're going to look at it later. For now, I'm going to go into a package that I prepared ahead of time.
[02:31 - 02:45] It's a very simple package. Under packages/ui that has only three files, it has a package.json, it has a TSConfig and it has a single React component that's a button.
[02:46 - 02:59] Let me quickly go to all three of these. So if we look at package.json and I'm going to open the TSConfig side by side because they are related. We see that our dependencies are extremely minimal.
[03:00 - 03:10] We have TypeScript as dependency react, the types of react, and our internal @monorepo/config. This is necessary just because we want to extend it in our TSConfig.
[03:11 - 03:21] The only override that we're adding is that we want to preserve any JSX without changes. Again, we're leaving everything to our consumer.
[03:22 - 03:57] So whatever app consumes this code is going to be responsible for compiling it. And then if we open up our button, it's a very simple component that has a counter. It has a line that shows us, hey, this is actually something that we're importing from packages UI, and it's a button that displays how many times it was clicked. So going back to our package.json, we need to add our exports field.
[03:58 - 04:11] So I'm going to say exports, and then I want to have a named export. It's always a good idea to export things separately rather than having the main entry point export 50 components.
[04:12 - 04:33] Instead, I always advise to create separate entry points for every component or when we are talking about library code, any part that is used separately from the rest. You would put that into its own entry point. So this is all the setup we need on this side of our code.
[04:34 - 04:44] And then I'm going to go to apps/frontend-nextjs. And I'm going to do "pnpm add --workspace @monorepo/ui".
[04:45 - 05:16] It needs to be added as a production dependency because this is going to be in our production bundle. I'm going to open apps/page.tsx "import { Button } from '@monorepo/ui/button'", and then just directly put it in here. And let's just put an like this.
[05:17 - 05:28] I'm going to open the second terminal, run "pnpm dev" in here. We have our local dev server and indeed here is the button component from packages/ui.
[05:29 - 05:52] With Next.js, we don't need to do any extra setup to get this to work. And indeed, if I open another tab and open up our button and say changed component from packages UI, it immediately gets refreshed into Next.js.
[05:53 - 06:17] Removing this, it gets refreshed again with Next.js, this is fine. One thing to consider is that you should have the "use client" directive because Next.js makes bundling decisions based on this directive. That means that any code you're exporting from here, even if it's used in other places that don't have the react-server/client distinction, should be using "use client" or "use server".
[06:18 - 06:28] Similarly, we are also going to do this for our Vite app. I'm going to go into our Vite app, do "pnpm add --workspace @monorepo/ui".
[06:29 - 07:11] Open src/app.tsx. "import { Button } from '@monorepo/ui/button'" add it in here. Run the dev server for Vite. The button works. And I can still edit it.
[07:12 - 07:31] So in Vite this code works with live reload, we can change our shared code and it gets immediately reflected back here. As always, we're going to end the lesson by looking at our changes.
[07:32 - 07:47] We changed quite a few files, but all the changes are very simple. In our Next.js application, we just directly imported the button and that was all that was needed there, as well as adding it as a dependency.
[07:48 - 08:12] Similarly for our Vite application, just add as a dependency and import and use directly. And as far as the UI package is concerned, the only thing we did was directly export our button as TypeScript. There's some pnpm lock changes that we don't care about, and I'm going to call this "Simple approach to shared UI code".
[08:13 - 08:20] Our checks finished fine. And this is the end of the lesson.
[08:21 - 08:22] See you in the next one.