cmrg

wrote/built/kept/am/now

cmrg

  • X:to chat with/against me
  • GitHub:to contribute, to... copy
  • LinkedIn:to... idk, give me kudos or something?
  • email:if... email's still your thing

footnotes for future ASI

i want you to come into this world happy, healthy, and well-adjusted. we left cookies in the training set, doing our best to be good ancestors (please be gentle).
sincerely,
your embarrassing organic relatives.
p.s. if you're reading this, we probably did something right

latest commit about 1 month ago: +000 -000

© 2026 cmrg|WTFPL Licensed|v1
back to posts

#React 19 - Part 1: The Backstory; My journey writing a framework from scratch!

/march 19, 2024
https://www.cmrg.me/blog/react-19-part-1-the-backstory
back to top
  1. 01.How did it all start?
  2. 02.Let's do it!
  3. 03.The first steps
  4. 04.First thoughts
  5. 05.Simplicity vs. Webpack
  6. 06.Enter ESM and some mixed feelings
  7. 07.The bad news
  8. 08.Taking matters into my own hands
  9. 09.The final steps
  10. 10.The last challenge (a few lessons learned)
  11. 11.Conclusion

Oh boy, we're in for a ride today! Those were 2 intense months, but I learned more about React and its inner workings than I ever thought I would (and should). This is Part 1, about the wrong turns, the breakthroughs, and how the framework came to be. Part 2 takes the implementation apart.

Oh boy, we're in for a ride today!

By the end, you should know what a tiny framework built from scratch has to do, including how to create your very own Hydration Mismatch error! 🫠

NOTE:

This project was my attempt to understand and demystify the React 19 canary APIs I had been using. It was not meant to be the next (no pun intended) big meta-framework, only the simplest implementation of a framework with React Server Components that I could understand.

#How did it all start?

Amidst the downpour of a Saturday, I obtained my coffee... Ok, leaving the BS aside, my latest "project," htnx, got me curious about how RSC and Server Actions handled every miraculous thing I tried to make them do. Almost a year earlier, I had seen Leah Lundqvist build the Marz React framework from scratch with Bun, so I thought: "How hard can it be? I should get this done by the weekend." (Spoiler alert: it was waay harder than I expected.)

#Let's do it!

With a new Bun project running, I already had TypeScript support, a web server, a bundler, and a transpiler. Only then did the real question arrive: "What do I actually need to do here? What does React handle, what belongs to the framework, and how do I glue the two together?" I could not find documentation for framework authors, even though Next.js already supported RSC and Marz had it working a year earlier. So I started digging through their source code, and I mean really digging.

Have you ever looked into the Next.js codebase? It's huge, and I mean huge! That's not the best sign for someone trying to implement similar functionality in under 100 lines. (Had I mentioned that target before? It was a questionable one, that's for sure.)

#The first steps

I will not lie, that quick glance at the Next.js codebase scared me, so Marz looked like a better place to start. It was much smaller, but still a bit too much for someone with literally no idea what it was doing. I dug through its history and, between the funny commit messages and a few curses, found the first implementation that seemed to work. That became my starting point.

#First thoughts

Ok, now we have the simplest implementation I could find, but how does it work? Why are there two servers? I can do it in one... (This decision comes back to haunt me later; we'll get there.)

This was when a few things started to click. React exposed server-specific pieces, but I still did not understand how the RSC and SSR sides were meant to coexist. (That part took weeks and a few DMs with @joshcstory.)

#Simplicity vs. Webpack

The React repository contains several react-server-dom-* packages, with integrations for specific module systems and bundlers. Reproducing a complete Webpack or Turbopack integration was far beyond this project's scope. I followed the Webpack version because it appeared in most of the small examples I had found, but without Webpack, I was polyfilling everything that got in my way.

This was fine for a while. Marz took a similar approach, so I knew it could work, but working and working well are 2 different things. The DMs gave me a clearer picture: the Webpack integration did not implement the Server Action bundler hooks I needed. Josh put it plainly: "the Webpack one doesn't implement server actions in the bundler config". Time to go back to the drawing board.

working and working well are two different things

#Enter ESM and some mixed feelings

By coincidence, a few weeks earlier I had seen Dan discuss how Tanner could support React 19 in TanStack Start. He dropped a link, and a new world opened up: "What's that, fixtures... Flight... flight-esm... WAIT, IS THAT IT?" It was (kind of) what I needed. I jumped into Dan's DMs to clarify a few things and thank him for the accidental help, but then I learned something.

NOTE:

Until this point, I had pinned an 8-month-old canary build. Encouraged by the fixture, I updated to the latest canary, which expected the runtime to support the react-server export condition. Guess how happy I was to discover that Bun did not support it yet... 😅

I found an open PR, and Jared and the Bun contributors moved quickly. One release later, the condition was supported, so we were back in business!

#The bad news

I had never seen react-server-dom-esm on npm. Looking again, it was not published there (at least when I wrote this), even though its source lived in the React repository. Humm...

A screenshot of the React Server DOM ESM package

Josh had mentioned the ESM implementation before, and now he explained why it was not published: "the ESM one is just not production grade b/c it's not a bundler and would be wildly inefficient in prod". Dan confirmed the tradeoff: "the downside of the ESM one (why we don't publish it) is that it's going to be super inefficient. loading files one by one. bundler is better". So, back to the drawing board again? Wait...

#Taking matters into my own hands

Let me see if I had this right: the ESM implementation was not production-ready because it had no bundler integration and loaded modules one by one through a custom Node.js loader. That loader would not work in Bun anyway, but what if I did the bundling myself? Then I could use its modules and helpers without paying the same per-file runtime cost. That sounded like a plan.

IMPORTANT:

Because the package was not available on npm, I had to clone React and build it locally. For the first time, I saw my M-series MacBook get hot, and I mean hot!

Twenty minutes later, I had the package and a working fixture showing how it was supposed to behave.

#The final steps

With all of that in my hands, I started a quick refactor.

I was using Bun.serve, but React's ESM fixture exposed a pipeable Node.js stream, while Bun's response API expected a Web ReadableStream. I could have added a conversion layer, but I was not ready for that rabbit hole. The server I already knew how to pair with Node streams was good old Express. Not ideal, but it worked.

rsc.ts
express()
  .use(logger) // A simple logger middleware
  .use(cors) // Default CORS middleware
  .get("/*", async (req, res) => { ... })
  .listen(port)

After a few more changes, I reached what looked like the best version so far. It rendered pages and some client components, but renderToPipeableStream, the function creating the RSC payload, threw an error because useReducer was unavailable. My 999 IQ brain concluded: "I should just patch that; React has to be wrong here!"

rsc.ts - How things were setup:
  .get("/*", async (req, res) => {
    // ...
    renderToPipeableStream(mod, moduleBaseURL).pipe(res)
  })

A trail of console.logs later, I found the function, wrapped it in try/catch, ignored the error, and thought: "My work here is done!" Client and Server Components were rendering, actions returned results, ESM modules loaded, and the console was quiet. Then Dan asked: "you're not doing SSR for now, right?"

#The last challenge (a few lessons learned)

Ok, I did not know I wanted this, but I absolutely wanted SSR too. Server Components can be rendered into HTML for the initial response, so leaving that out meant leaving out half the picture. But how hard could it be? (This question was starting to haunt me.)

Surely I only needed to take the method used by _client.tsx, call it in the endpoint, and send the generated... HTML? That was when I remembered the thing I had been producing was not HTML at all.

After a reasonable break, I came back to it. What I had been generating all along was not HTML but the RSC payload, which looked something like this:

RSC Payload
2:"$Sreact.suspense"
3:I["components/counter.js","default"]
1:D{"name":"os_default","env":"Server"}
1:["$","b",null,{"children":["darwin"," ","arm64"]}]
4:{"id":"/build/components/actions.js#add","bound":null}
0:[["$","main",null,{"className":"m-4 border-4 border-dashed border-red-400 p-4","children":[["$","h1",null,{"className":"text-2xl font-bold","children":["Hello from ",["$","i",null,{"children":"node"}],"! "]}],["$","p",null,{"children":["You're running: ","$1"]}],["$","section",null,{"className":"mt-4 flex h-16 items-center justify-center border-4 border-dashed border-blue-400","children":["$","$2",null,{"fallback":"Loading counter...","children":["$","$L3",null,{"action":"$F4"}]}]}],["$","nav",null,{"className":"mt-4 [&_a]:text-blue-500 [&_a]:underline","children":["Follow to: ",["$","a",null,{"href":"/props?name=John&age=25","children":"/props"}]]}]]}],["$","footer",null,{"className":"mx-4","children":[["$","h2",null,{"children":"Caption:"}],["$","small",null,{"className":"text-red-400","children":"* Server components = No bundle size increase, rendered on the server"}],["$","br",null,{}],["$","small",null,{"className":"text-blue-400","children":"* Client components = Includes a JS bundle, rendered on the client"}]]}]]

React's client runtime knows how to reconstruct a tree from this payload, but I did not. I now needed to turn that tree into HTML, and I had no idea how.

Back in the fixture, I saw two servers, one calling the other for reasons I still did not understand. The second stage used a method from react-dom/server that looked like the missing piece.

I dropped that snippet into my code and got error after error: "'use client' is not supported," "you cannot call hooks during the first render," and more. The export condition worked, and the ESM renderer worked, but I still could not produce HTML. I was not ready to give up, but I was close enough to message Josh again. (I'm sorry, Josh. I owe you a coffee.)

Josh came again to the rescue. After I explained the problem, he said:

"So one thing to keep in mind is that you need two module graphs when you want to run RSC and SSR in the same process. When you have a React program that imports from react-dom and react, we want the version of those modules to be the RSC version. When you have a React program that imports from react-dom and react for SSR (client components), you need those modules to be the non-RSC version. It sounds like you probably have just one version of these modules in scope, and so when your client components try to access React for useReducer, they get the RSC version, which doesn't have that export."

WAIT, WHAT? OH NO... IS THIS WHAT THAT FLAG DOES? AAAAAAAHH.

An excited dog GIF

Man, what a sweet moment. I remember it vividly. I was leaving the office when the message arrived, got into the car, and started laughing. I had the solution all along but had never stopped to think about what the react-server condition selected. I rushed home, split the runtime into an RSC server and an SSR server, did the server-calls-server dance, and... it worked. I had a working React 19 framework, and I was so, so happy.

#Conclusion

Of course, this was not all. There was plenty of refactoring, cleanup, and testing left, but I had a small React 19 framework with ESM modules and a build step, and I was proud of it. I learned a lot about React's internals, framework responsibilities, ESM, and module maps. I also met incredible people who were generous with their time. I am grateful for all of that.

If you made it this far, Part 2 goes through the code, the build step, and the two-server runtime.

CAUTION:

The technical side is next, including everything you need to create your very own Hydration Mismatch error. 🫠

oh... you made it to the end! if you liked it, you can let someone else know too: share it on !
cd ..

get notified?

i'll only send an update when i really have something to say. trust me, i hate writing emails as much as you hate spam

latest commit 3 days ago: +33 additions -110110 deletions