- Notifications
You must be signed in to change notification settings - Fork20
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
-
RSC From Scratch. Part 1: Server ComponentsIn this technical deep dive, we'll implement a very simplified version ofReact Server Components (RSC) from scratch.
Seriously, this is a deep dive!This deep dive doesn't explain the benefits of React Server Components, how to implement an app using RSC, or how to implement a framework using them. Instead, it walks you through the process of "inventing" them on your own from scratch. 🔬This is a deep dive for people who like to learn new technologies by implementing them from scratch. 🚧This deep dive is not intended as an introduction to how touse Server Components. We are working to document Server Components on the React website. In the meantime, if your framework supports Server Components, please refer to its docs. 😳For pedagogical reasons, our implementation will be significantly less efficient than the real one used by React. Let’s jump back in time...Suppose that you woke up one morning and found out it's 2003 again. Web development is still in its infancy. Let's say you want to create a personal blog website that shows content from text files on your server. In PHP, it could look like this: <?php$author ="Jae Doe";$post_content = @file_get_contents("./posts/hello-world.txt");?><html> <head> <title>My blog</title> </head> <body> <nav> <a href="/">Home</a> <hr> </nav> <article><?phpechohtmlspecialchars($post_content);?> </article> <footer> <hr> <p><i>(c)<?phpechohtmlspecialchars($author);?>,<?phpechodate("Y");?></i></p> </footer> </body></html> (We're going to pretend that tags like When you open import{createServer}from'http';import{readFile}from'fs/promises';importescapeHtmlfrom'escape-html';createServer(async(req,res)=>{constauthor="Jae Doe";constpostContent=awaitreadFile("./posts/hello-world.txt","utf8");sendHTML(res,`<html> <head> <title>My blog</title> </head> <body> <nav> <a href="/">Home</a> <hr /> </nav> <article>${escapeHtml(postContent)} </article> <footer> <hr> <p><i>(c)${escapeHtml(author)},${newDate().getFullYear()}</i></p> </footer> </body> </html>`);}).listen(8080);functionsendHTML(res,html){res.setHeader("Content-Type","text/html");res.end(html);} Open this example in a sandbox. Imagine that you could take a CD-ROM with a working Node.js engine back to 2003, and you could run this code on the server. If you wanted to bring a React-flavored paradigm to that world, what features would you add, and in what order? Step 1: Let's invent JSXThe first thing that's not ideal about the code above is direct string manipulation. Notice you've had to call One way you could solve this is by splitting your logic from your "template", and then introducing a separate templating language that provides a way to inject dynamic values for text and attributes, escapes text content safely, and provides domain-specific syntax for conditions and loops. That's the approach taken by some of the most popular server-centric frameworks in 2000s. However, your existing knowledge of React might inspire you to do this instead: createServer(async(req,res)=>{constauthor="Jae Doe";constpostContent=awaitreadFile("./posts/hello-world.txt","utf8");sendHTML(res,<html><head><title>My blog</title></head><body><nav><ahref="/">Home</a><hr/></nav><article>{postContent}</article><footer><hr/><p><i>(c){author},{newDate().getFullYear()}</i></p></footer></body></html>);}).listen(8080); This looks similar, but our "template" is not a string anymore. Instead of writing string interpolation code, we're putting a subset of XML into JavaScript. In other words, we've just "invented" JSX. JSX lets you keep markup close to the related rendering logic, but unlike string interpolation, it prevents mistakes like mismatching open/close HTML tags or forgetting to escape text content. Under the hood, JSX produces a tree of objects that look like this: // Slightly simplified{$$typeof:Symbol.for("react.element"),// Tells React it's a JSX element (e.g. <html>)type:'html',props:{children:[{$$typeof:Symbol.for("react.element"),type:'head',props:{children:{$$typeof:Symbol.for("react.element"),type:'title',props:{children:'My blog'}}}},{$$typeof:Symbol.for("react.element"),type:'body',props:{children:[{$$typeof:Symbol.for("react.element"),type:'nav',props:{children:[{$$typeof:Symbol.for("react.element"),type:'a',props:{href:'/',children:'Home'}},{$$typeof:Symbol.for("react.element"),type:'hr',props:null}]}},{$$typeof:Symbol.for("react.element"),type:'article',props:{children:postContent}},{$$typeof:Symbol.for("react.element"),type:'footer',props:{/* ...And so on... */}}]}}]}} However, in the end what you need to send to the browser is HTML — not a JSON tree. (At least, for now!) Let's write a function that turns your JSX to an HTML string. To do this, we'll need to specify how different types of nodes (a string, a number, an array, or a JSX node with children) should turn into pieces of HTML: functionrenderJSXToHTML(jsx){if(typeofjsx==="string"||typeofjsx==="number"){// This is a string. Escape it and put it into HTML directly.returnescapeHtml(jsx);}elseif(jsx==null||typeofjsx==="boolean"){// This is an empty node. Don't emit anything in HTML for it.return"";}elseif(Array.isArray(jsx)){// This is an array of nodes. Render each into HTML and concatenate.returnjsx.map((child)=>renderJSXToHTML(child)).join("");}elseif(typeofjsx==="object"){// Check if this object is a React JSX element (e.g. <div />).if(jsx.$$typeof===Symbol.for("react.element")){// Turn it into an an HTML tag.lethtml="<"+jsx.type;for(constpropNameinjsx.props){if(jsx.props.hasOwnProperty(propName)&&propName!=="children"){html+=" ";html+=propName;html+="=";html+=escapeHtml(jsx.props[propName]);}}html+=">";html+=renderJSXToHTML(jsx.props.children);html+="</"+jsx.type+">";returnhtml;}elsethrownewError("Cannot render an object.");}elsethrownewError("Not implemented.");} Open this example in a sandbox. Give this a try and see the HTML being rendered and served! Turning JSX into an HTML string is usually known as "Server-Side Rendering" (SSR).It is important note that RSC and SSR are two very different things (that tend to be used together). In this guide, we'restarting from SSR because it's a natural first thing you might try to do in a server environment. However, this is only the first step, and you will see significant differences later on. Step 2: Let's invent componentsAfter JSX, the next feature you'll probably want is components. Regardless of whether your code runs on the client or on the server, it makes sense to split the UI apart into different pieces, give them names, and pass information to them by props. Let's break the previous example apart into two components called functionBlogPostPage({ postContent, author}){return(<html><head><title>My blog</title></head><body><nav><ahref="/">Home</a><hr/></nav><article>{postContent}</article><Footerauthor={author}/></body></html>);}functionFooter({ author}){return(<footer><hr/><p><i> (c){author}{newDate().getFullYear()}</i></p></footer>);} Then, let's replace inline JSX tree we had with createServer(async(req,res)=>{constauthor="Jae Doe";constpostContent=awaitreadFile("./posts/hello-world.txt","utf8");sendHTML(res,<BlogPostPagepostContent={postContent}author={author}/>);}).listen(8080); If you try to run this code without any changes to your
The problem is that our if(jsx.$$typeof===Symbol.for("react.element")){// Existing code that handles HTML tags (like <p>).lethtml="<"+jsx.type;// ...html+="</"+jsx.type+">";returnhtml;} But here, if(jsx.$$typeof===Symbol.for("react.element")){if(typeofjsx.type==="string"){// Is this a tag like <div>?// Existing code that handles HTML tags (like <p>).lethtml="<"+jsx.type;// ...html+="</"+jsx.type+">";returnhtml;}elseif(typeofjsx.type==="function"){// Is it a component like <BlogPostPage>?// Call the component with its props, and turn its returned JSX into HTML.constComponent=jsx.type;constprops=jsx.props;constreturnedJsx=Component(props);returnrenderJSXToHTML(returnedJsx);}elsethrownewError("Not implemented.");} Now, if you encounter a JSX element like This change alone is enough to add support for components and passing props. Check it out: Open this example in a sandbox. Step 3: Let's add some routingNow that we've got basic support for components working, it would be nice to add a few more pages to the blog. Let's say a URL like Currently, the functionBlogLayout({ children}){constauthor="Jae Doe";return(<html><head><title>My blog</title></head><body><nav><ahref="/">Home</a><hr/></nav><main>{children}</main><Footerauthor={author}/></body></html>);} We'll change the functionBlogPostPage({ postSlug, postContent}){return(<section><h2><ahref={"/"+postSlug}>{postSlug}</a></h2><article>{postContent}</article></section>);} Here is how Let's also add anew functionBlogIndexPage({ postSlugs, postContents}){return(<section><h1>Welcome to my blog</h1><div>{postSlugs.map((postSlug,index)=>(<sectionkey={postSlug}><h2><ahref={"/"+postSlug}>{postSlug}</a></h2><article>{postContents[index]}</article></section>))}</div></section>);} Then you can nest it inside Finally, let's change the server handler to pick the page based on the URL, load the data for it, and render that page inside the layout: createServer(async(req,res)=>{try{consturl=newURL(req.url,`http://${req.headers.host}`);// Match the URL to a page and load the data it needs.constpage=awaitmatchRoute(url);// Wrap the matched page into the shared layout.sendHTML(res,<BlogLayout>{page}</BlogLayout>);}catch(err){console.error(err);res.statusCode=err.statusCode??500;res.end();}}).listen(8080);asyncfunctionmatchRoute(url){if(url.pathname==="/"){// We're on the index route which shows every blog post one by one.// Read all the files in the posts folder, and load their contents.constpostFiles=awaitreaddir("./posts");constpostSlugs=postFiles.map((file)=>file.slice(0,file.lastIndexOf(".")));constpostContents=awaitPromise.all(postSlugs.map((postSlug)=>readFile("./posts/"+postSlug+".txt","utf8")));return<BlogIndexPagepostSlugs={postSlugs}postContents={postContents}/>;}else{// We're showing an individual blog post.// Read the corresponding file from the posts folder.constpostSlug=sanitizeFilename(url.pathname.slice(1));try{constpostContent=awaitreadFile("./posts/"+postSlug+".txt","utf8");return<BlogPostPagepostSlug={postSlug}postContent={postContent}/>;}catch(err){throwNotFound(err);}}}functionthrowNotFound(cause){constnotFound=newError("Not found.",{ cause});notFound.statusCode=404;thrownotFound;} Now you can navigate around the blog. However, the code is getting a bit verbose and clunky. We'll solve that next. Open this example in a sandbox. Step 4: Let's invent async componentsYou might have noticed that this part of the It would be nice if we could somehow make this a reusable component. However, even if you extracted its rendering logic into a separate functionPost({ slug, content}){// Someone needs to pass down the `content` prop from the file :-(return(<section><h2><ahref={"/"+slug}>{slug}</a></h2><article>{content}</article></section>)} Currently, the logic for loading Or can we?... If you are used to client-side React, you might be used to the idea that you can't call an API like But if you tried to explain this to someone in 2003, they would find this limitation rather odd. You can't Recall that we're approaching everything from the first principles. For now, we areonly targeting the server environment, so we don't need to limit our components to code that runs in the browser. It is also perfectly fine for a component to be asynchronous, since the server can just wait with emitting HTML for it until its data has loaded and is ready to display. Let's remove the asyncfunctionPost({ slug}){letcontent;try{content=awaitreadFile("./posts/"+slug+".txt","utf8");}catch(err){throwNotFound(err);}return(<section><h2><ahref={"/"+slug}>{slug}</a></h2><article>{content}</article></section>)} Similarly, let's make asyncfunctionBlogIndexPage(){constpostFiles=awaitreaddir("./posts");constpostSlugs=postFiles.map((file)=>file.slice(0,file.lastIndexOf(".")));return(<section><h1>Welcome to my blog</h1><div>{postSlugs.map((slug)=>(<Postkey={slug}slug={slug}/>))}</div></section>);} Now that functionRouter({ url}){letpage;if(url.pathname==="/"){page=<BlogIndexPage/>;}else{constpostSlug=sanitizeFilename(url.pathname.slice(1));page=<BlogPostPagepostSlug={postSlug}/>;}return<BlogLayout>{page}</BlogLayout>;} Finally, the top-level server handler can delegate all the rendering to the createServer(async(req,res)=>{try{consturl=newURL(req.url,`http://${req.headers.host}`);awaitsendHTML(res,<Routerurl={url}/>);}catch(err){console.error(err);res.statusCode=err.statusCode??500;res.end();}}).listen(8080); But wait, we need toactually make Let's find the place in our }elseif(typeofjsx.type==="function"){constComponent=jsx.type;constprops=jsx.props;constreturnedJsx=Component(props);// <--- This is where we're calling componentsreturnrenderJSXToHTML(returnedJsx);}elsethrownewError("Not implemented."); Since component functions can now be asynchronous, let's add an // ...constreturnedJsx=awaitComponent(props);// ... This means asyncfunctionrenderJSXToHTML(jsx){// ...} With this change, any component in the tree can be Notice how, in the new code, there is no special logic to "prepare" all the file contents for Open this example in a sandbox.
Step 5: Let's preserve state on navigationSo far, our server can only render a route to an HTML string: asyncfunctionsendHTML(res,jsx){consthtml=awaitrenderJSXToHTML(jsx);res.setHeader("Content-Type","text/html");res.end(html);} This is great for the first load — the browser is optimized to show HTML as quickly as possible — but it's not ideal for navigations.We'd like to be able to update "just the parts that changed"in-place, preserving the client-side state both inside and around them (e.g. an input, a video, a popup, etc). This will also let mutations (e.g. adding a comment to a blog post) feel fluid. To illustrate the problem, let'sadd an <nav><ahref="/">Home</a><hr/><input/><hr/></nav> Notice how the state of the input gets "blown away" every time you navigate around the blog: 1.mp4This might be OK for a simple blog, but if you want to be able to build more interactive apps, at some point this behavior becomes a dealbreaker. You want to let the user navigate around the app without constantly losing local state. We're going to fix this in three steps:
Step 5.1: Let's intercept navigationsWe're gonna need some client-side logic, so we'll add a asyncfunctionnavigate(pathname){// TODO}window.addEventListener("click",(e)=>{// Only listen to link clicks.if(e.target.tagName!=="A"){return;}// Ignore "open in a new tab".if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey){return;}// Ignore external URLs.consthref=e.target.getAttribute("href");if(!href.startsWith("/")){return;}// Prevent the browser from reloading the page but update the URL.e.preventDefault();window.history.pushState(null,null,href);// Call our custom logic.navigate(href);},true);window.addEventListener("popstate",()=>{// When the user presses Back/Forward, call our custom logic too.navigate(window.location.pathname);}); In the letcurrentPathname=window.location.pathname;asyncfunctionnavigate(pathname){currentPathname=pathname;// Fetch HTML for the route we're navigating to.constresponse=awaitfetch(pathname);consthtml=awaitresponse.text();if(pathname===currentPathname){// Get the part of HTML inside the <body> tag.constbodyStartIndex=html.indexOf("<body>")+"<body>".length;constbodyEndIndex=html.lastIndexOf("</body>");constbodyHTML=html.slice(bodyStartIndex,bodyEndIndex);// Replace the content on the page.document.body.innerHTML=bodyHTML;}} Open this example in a sandbox. This code isn't quite production-ready (for example, it doesn't change Step 5.2: Let's send JSX over the wireRemember our earlier peek at the object tree that JSX produces: {$$typeof:Symbol.for("react.element"),type:'html',props:{children:[{$$typeof:Symbol.for("react.element"),type:'head',props:{// ... And so on ... We're going to add a new mode to our server. When the request ends with To start off, let's change our server code to call a new createServer(async(req,res)=>{try{consturl=newURL(req.url,`http://${req.headers.host}`);if(url.pathname==="/client.js"){// ...}elseif(url.searchParams.has("jsx")){url.searchParams.delete("jsx");// Keep the url passed to the <Router> cleanawaitsendJSX(res,<Routerurl={url}/>);}else{awaitsendHTML(res,<Routerurl={url}/>);}// ... In asyncfunctionsendJSX(res,jsx){constjsxString=JSON.stringify(jsx,null,2);// Indent with two spaces.res.setHeader("Content-Type","application/json");res.end(jsxString);} We'll keep referring to this as "sending JSX", but we're not sending the JSX syntax itself (like Let's change the client code to see what passes through the network: asyncfunctionnavigate(pathname){currentPathname=pathname;constresponse=awaitfetch(pathname+"?jsx");constjsonString=awaitresponse.text();if(pathname===currentPathname){alert(jsonString);}} Give this a try. If you load the index {"key":null,"ref":null,"props":{"url":"http://localhost:3000/hello-world"},// ...} That's not very useful — we were hoping to get a JSX tree like Initially, our JSX looks like this: <Routerurl="http://localhost:3000/hello-world"/>// {// $$typeof: Symbol.for('react.element'),// type: Router,// props: { url: "http://localhost:3000/hello-world" }},// ...// } It is "too early" to turn this JSX into JSON for the client because we don't know what JSX the If we call the <BlogLayout><BlogIndexPage/></BlogLayout> Again, it is "too early" to turn this JSX into JSON for the client because we don't know what (An experienced React user might object: can't we send their code to the client so that it can execute them? Hold that thought until the next part of this series! But even that would only work for At the end of this process, we end up with a JSX tree that does not reference any server-only code. For example: <html><head>...</head><body><nav><ahref="/">Home</a><hr/></nav><main><section><h1>Welcome to my blog</h1><div> ...</div></main><footer><hr/><p><i> (c) Jae Doe 2003</i></p></footer></body></html> Now,that is the kind of tree that we can pass to Let's write a function called Structurally, this function is similar to asyncfunctionrenderJSXToClientJSX(jsx){if(typeofjsx==="string"||typeofjsx==="number"||typeofjsx==="boolean"||jsx==null){// Don't need to do anything special with these types.returnjsx;}elseif(Array.isArray(jsx)){// Process each item in an array.returnPromise.all(jsx.map((child)=>renderJSXToClientJSX(child)));}elseif(jsx!=null&&typeofjsx==="object"){if(jsx.$$typeof===Symbol.for("react.element")){if(typeofjsx.type==="string"){// This is a component like <div />.// Go over its props to make sure they can be turned into JSON.return{ ...jsx,props:awaitrenderJSXToClientJSX(jsx.props),};}elseif(typeofjsx.type==="function"){// This is a custom React component (like <Footer />).// Call its function, and repeat the procedure for the JSX it returns.constComponent=jsx.type;constprops=jsx.props;constreturnedJsx=awaitComponent(props);returnrenderJSXToClientJSX(returnedJsx);}elsethrownewError("Not implemented.");}else{// This is an arbitrary object (for example, props, or something inside of them).// Go over every value inside, and process it too in case there's some JSX in it.returnObject.fromEntries(awaitPromise.all(Object.entries(jsx).map(async([propName,value])=>[propName,awaitrenderJSXToClientJSX(value),])));}}elsethrownewError("Not implemented");} Next, let's edit asyncfunctionsendJSX(res,jsx){constclientJSX=awaitrenderJSXToClientJSX(jsx);constclientJSXString=JSON.stringify(clientJSX,null,2);// Indent with two spacesres.setHeader("Content-Type","application/json");res.end(clientJSXString);} Open this example in a sandbox. Now clicking on a link shows an alert with a tree that looks similar to HTML — which means we're ready to try diffing it!
Step 5.3: Let's apply JSX updates on the clientStrictly saying, we don't have to use React to diff JSX. So far, our JSX nodesonly contain built-in browser components like Our app is server-rendered to HTML. In order to ask React to take over managing a DOM node that it didn't create (such as a DOM node created by the browser from HTML), you need to provide React with the initial JSX corresponding to that DOM node. Imagine a contractor asking you to see the house plan before doing renovations. They prefer to know the original plan to make future changes safely. Similarly, React walks over the DOM to see which part of the JSX every DOM node corresponds to. This lets React attach event handlers to the DOM nodes, making them interactive, or update them later. They're nowhydrated, like plants coming alive with water. Traditionally, to hydrate server-rendered markup, you would call // Traditionally, you would hydrate like thishydrateRoot(document,<App/>); The problem is we don't have a root component like import{hydrateRoot}from'react-dom/client';constroot=hydrateRoot(document,getInitialClientJSX());functiongetInitialClientJSX(){// TODO: return the <html>...</html> client JSX tree mathching the initial HTML} This would be extremely fast because right now, there are no components in the client JSX tree at all. React would walk the DOM tree and JSX tree in a near-instant, and build its internal data structure that's necessary to update that tree later on. Then, whenever the user navigates, we'd fetch the JSX for the next page and update the DOM with asyncfunctionnavigate(pathname){currentPathname=pathname;constclientJSX=awaitfetchClientJSX(pathname);if(pathname===currentPathname){root.render(clientJSX);}}asyncfunctionfetchClientJSX(pathname){// TODO: fetch and return the <html>...</html> client JSX tree for the next route} This will achieve what we wanted — it will update the DOM in the same way React normally does, without destroying the state. Now let's figure out how to implement these two functions. Step 5.3.1: Let's fetch JSX from the serverWe'll start with First, let's recall how our asyncfunctionsendJSX(res,jsx){constclientJSX=awaitrenderJSXToClientJSX(jsx);constclientJSXString=JSON.stringify(clientJSX);res.setHeader("Content-Type","application/json");res.end(clientJSXString);} On the client, we're going to call this endpoint, and then feed the response to asyncfunctionfetchClientJSX(pathname){constresponse=awaitfetch(pathname+"?jsx");constclientJSXString=awaitresponse.text();constclientJSX=JSON.parse(clientJSXString);returnclientJSX;} If youtry this implementation, you'll see an error whenever you click a link and attempt to render the fetched JSX:
Here's why. The object we're passing to {$$typeof:Symbol.for("react.element"),type:'html',props:{// ... However, if you look at the {type:'html',props:{// ... Without This is an intentional security mechanism. By default, React refuses to treat arbitrary JSON objects fetched from the network as JSX tags. The trick is that a Symbol value like However, wedid actually create these JSX nodes (on the server) anddo want to render them on the client. So we need to adjust our logic to "carry over" the Luckily, this is not too difficult to fix. asyncfunctionsendJSX(res,jsx){// ...constclientJSXString=JSON.stringify(clientJSX,stringifyJSX);// Notice the second argument// ...}functionstringifyJSX(key,value){if(value===Symbol.for("react.element")){// We can't pass a symbol, so pass our magic string instead.return"$RE";// Could be arbitrary. I picked RE for React Element.}elseif(typeofvalue==="string"&&value.startsWith("$")){// To avoid clashes, prepend an extra $ to any string already starting with $.return"$"+value;}else{returnvalue;}} On the client, we'll pass areviver function to asyncfunctionfetchClientJSX(pathname){// ...constclientJSX=JSON.parse(clientJSXString,parseJSX);// Notice the second argument// ...}functionparseJSX(key,value){if(value==="$RE"){// This is our special marker we added on the server.// Restore the Symbol to tell React that this is valid JSX.returnSymbol.for("react.element");}elseif(typeofvalue==="string"&&value.startsWith("$$")){// This is a string starting with $. Remove the extra $ added by the server.returnvalue.slice(1);}else{returnvalue;}} Open this example in a sandbox. Now you can navigate between the pages again — but the updates are fetched as JSX and applied on the client! If you type into the input and then click a link, you'll notice the Step 5.3.2: Let's inline the initial JSX into the HTMLWe still have this bit of code: constroot=hydrateRoot(document,getInitialClientJSX());functiongetInitialClientJSX(){returnnull;// TODO} We need to hydrate the root with the initial client JSX, but where do we get that JSX on the client? Our page is server-rendered to HTML; however, for further navigations we need to tell React what the initial JSX for the page was. In some cases, it might be possible to partially reconstruct from the HTML, but not always—especially when we start adding interactive features in the next part of this series. We also don't want tofetch it since it would create an unnecessary waterfall. In traditional SSR with React, you also encounter a similar problem, but for data. You need to have the data for the page so that components can hydrate and return their initial JSX. In our case, there are no components on the page so far (at least, none that run in the browser), so nothing needs to run — but there is also no code on the client that knows how to generate that initial JSX. To solve this, we're going to assume that the string with the initial JSX is available as a global variable on the client: constroot=hydrateRoot(document,getInitialClientJSX());functiongetInitialClientJSX(){constclientJSX=JSON.parse(window.__INITIAL_CLIENT_JSX_STRING__,reviveJSX);returnclientJSX;} On the server, we will modify the asyncfunctionsendHTML(res,jsx){lethtml=awaitrenderJSXToHTML(jsx);// Serialize the JSX payload after the HTML to avoid blocking paint:constclientJSX=awaitrenderJSXToClientJSX(jsx);constclientJSXString=JSON.stringify(clientJSX,stringifyJSX);html+=`<script>window.__INITIAL_CLIENT_JSX_STRING__ = `;html+=JSON.stringify(clientJSXString).replace(/</g,"\\u003c");html+=`</script>`;// ... Finally, we need a fewsmall adjustments to how we generate HTML for text nodes so that React can hydrate them. Open this example in a sandbox. Now you can type into an input, and its state is no longer lost between navigations: 2.mp4That's the goal we originally set out to accomplish! Of course, preserving the state of this particular input isn't the point—the important part is that our app can now refresh and navigate "in-place" on any page, and not worry about destroying any state.
Step 6: Let's clean things upNow that our code actuallyworks, we're going to move the architecture a tiny bit closer to the real RSC. We're still not going to implement complex mechanisms like streaming yet, but we'll fix a few flaws and prepare for the next wave of features. Step 6.1: Let's avoid duplicating workHave another look athow we're producing the initial HTML: asyncfunctionsendHTML(res,jsx){// We need to turn <Router /> into "<html>...</html>" (a string):lethtml=awaitrenderJSXToHTML(jsx);// We *also* need to turn <Router /> into <html>...</html> (an object):constclientJSX=awaitrenderJSXToClientJSX(jsx); Suppose First, we call What if we generated the client JSX treefirst? asyncfunctionsendHTML(res,jsx){// 1. Let's turn <Router /> into <html>...</html> (an object) first:constclientJSX=awaitrenderJSXToClientJSX(jsx); By this point, all our components have executed. Then, let's generate HTML fromthat tree: asyncfunctionsendHTML(res,jsx){// 1. Let's turn <Router /> into <html>...</html> (an object) first:constclientJSX=awaitrenderJSXToClientJSX(jsx);// 2. Turn that <html>...</html> into "<html>...</html>" (a string):lethtml=awaitrenderJSXToHTML(clientJSX);// ... Now components are only called once per request, as they should be. Open this example in a sandbox. Step 6.2: Let's use React to render HTMLInitially, we needed a custom import{renderToString}from'react-dom/server';// ...asyncfunctionsendHTML(res,jsx){constclientJSX=awaitrenderJSXToClientJSX(jsx);lethtml=renderToString(clientJSX);// ... Open this example in a sandbox. Notice a parallel with the client code. Even though we've implemented new features (like In a traditional server-rendered React app, you'd call In a traditional server-rendered React app, components execute in the same wayboth on the server and the client. But in our approach, components like As far as Step 6.3: Let's split the server in twoIn the previous step, we've decoupled running components from generating HTML:
Since these steps are independent, they don't have to be done in the same process or even on the same machine.
We'll run them both in parallel in our "scripts":{"start":"concurrently \"npm run start:ssr\" \"npm run start:rsc\"","start:rsc":"nodemon -- --experimental-loader ./node-jsx-loader.js ./server/rsc.js","start:ssr":"nodemon -- --experimental-loader ./node-jsx-loader.js ./server/ssr.js"}, In this example, they'll be on the same machine, but you could host them separately. The RSC server is the one that renders our components. It's only capable of serving their JSX output: // server/rsc.jscreateServer(async(req,res)=>{try{consturl=newURL(req.url,`http://${req.headers.host}`);awaitsendJSX(res,<Routerurl={url}/>);}catch(err){console.error(err);res.statusCode=err.statusCode??500;res.end();}}).listen(8081);functionRouter({ url}){// ...}// ...// ... All other components we have so far ...// ...asyncfunctionsendJSX(res,jsx){// ...}functionstringifyJSX(key,value){// ...}asyncfunctionrenderJSXToClientJSX(jsx){// ...} The other server is the SSR server. The SSR server is the server that our users will hit. It asks the RSC server for JSX, and then either serves that JSX as a string (for navigations between pages), or turns it into HTML (for the initial load): // server/ssr.jscreateServer(async(req,res)=>{try{consturl=newURL(req.url,`http://${req.headers.host}`);if(url.pathname==="/client.js"){// ...}// Get the serialized JSX response from the RSC serverconstresponse=awaitfetch("http://127.0.0.1:8081"+url.pathname);if(!response.ok){res.statusCode=response.status;res.end();return;}constclientJSXString=awaitresponse.text();if(url.searchParams.has("jsx")){// If the user is navigating between pages, send that serialized JSX as isres.setHeader("Content-Type","application/json");res.end(clientJSXString);}else{// If this is an initial page load, revive the tree and turn it into HTMLconstclientJSX=JSON.parse(clientJSXString,parseJSX);lethtml=renderToString(clientJSX);html+=`<script>window.__INITIAL_CLIENT_JSX_STRING__ = `;html+=JSON.stringify(clientJSXString).replace(/</g,"\\u003c");html+=`</script>`;// ...res.setHeader("Content-Type","text/html");res.end(html);}}catch(err){// ...}}).listen(8080); Open this example in a sandbox. We're going to keep this separation between RSC and "the rest of the world" (SSR and user machine) throughout this series. Its importance will become clearer in the next parts when we start adding features to both of these worlds, and tying them together. (Strictly speaking, it is technically possible to run RSC and SSR within the same process, but their module environments would have to be isolated from each other. This is an advanced topic, and is out of scope of this post.) RecapAnd we're done for today! It might seem like we've written a lot of code, but we really haven't:
Have a read through them. To help the data flow "settle" in our minds, let's draw a few diagrams. Here is what happens during the first page load: ![]() And here is what happens when you navigate between pages: ![]() Finally, let's establish some terminology:
ChallengesIf reading through this post wasn't enough to satisfy your curiosity, why not play with thefinal code? Here's a few ideas for things you can try:
Have fun! |
BetaWas this translation helpful?Give feedback.
All reactions
👍 7🎉 48❤️ 19