DEV Community 👩‍💻👨‍💻

Cover image for Client-side Routing without the JavaScript
Ryan Carniato for This is Learning

Posted on • Updated on

Client-side Routing without the JavaScript

It's been a while since I wrote a piece about a SolidJS technology innovation. It's been two years now since we added Suspense on the server with Streaming SSR. And even longer to go back to when we first introduced Suspense for data fetching and concurrent rendering back in 2019.

While React had introduced these concepts, implementing them for a fine-grained reactive system was a whole other sort of beast. Requiring a little imagination and completely different solutions that avoided diffing.

And that is a similar feeling to the exploration we've been doing recently. Inspired equal parts from React Server Components and Island solutions like Marko and Astro, Solid has made it's first steps into Partial Hydration. (comparison at the bottom)


SolidStart

Image description

Since releasing Solid 1.0 I've been kinda swamped. Between keeping open issues down and trying to check off more boxes for adoption I definitely have felt spread thin. Everything pointed to the need for a SSR meta-framework, an effort I started even before the 1.0 release.

The community stepped up to help. But ultimately, for getting the beta out the door I would become the blocker. And Nikhil Saraf, never one to sit still, having recently been introduced to Fresh wanted to see if he couldn't just add Islands to SolidStart.

Wanting to keep things focused on a release, I agreed but told him to time-box it as I'd need his help the next day. The next day he showed me a demo where he did not only add Islands, recreating the Fresh experience, but he had added client-side routing.


Accidental Islands

Image description

Now the demo was rough, but it was impressive. He'd taken one of my Hackernews demos and re-implemented the recursive Islands. What are recursive Islands.. that's when you project Islands in Islands:

function MyServerComponent(props) {
  return <>{ props.data && 
    <MyClientIsland>
      <MyServerComponent data={props.data.childData} />
    </MyClientIsland>
  }</>
}
Enter fullscreen mode Exit fullscreen mode

Why would you want this? It would be nice to wrap server rendered content with interactivity without completely losing our low JavaScript for the whole subtree.

However, there is a rule with Islands that you cannot import and use Server only components in them. The reason is you don't want the client to be able to pass state to them. Why? Well if the client could pass state to them then they'd need to be able to update and since the idea is to not send this JavaScript to the browser this wouldn't work. Luckily props.children enforces this boundary pretty well. (Assuming you disallow passing render functions/render props across Island boundaries).

function MyClientIsland() {
  const [state, setState] = createSignal();

  // can't pass props to the children
  return <div>{props.children}</div>
}
Enter fullscreen mode Exit fullscreen mode

How was he able to make this demo in such short order? Well, it was by chance. Solid's hydration works off of matching hierarchical IDs to templates instantiated in the DOM. They look something like this:

<div data-hk="0-0-1-0-2" />
Enter fullscreen mode Exit fullscreen mode

Each template increments a count and each nested component adds another digit. This is essential for our single-pass hydration. After all JSX can be created in any order and Suspense boundaries resolved at any time.

But at a given depth all ids will be assigned in the same order client or server.

function Component() {
  const anotherDiv = <div data-hk="1" /> 
  return <div data-hk="2">{anotherDiv}</div>
}

// output
<div data-hk="2">
  <div data-hk="1" />
</div>
Enter fullscreen mode Exit fullscreen mode

Additionally, I had added a <NoHydration> component to suppress these IDs so that we could skip hydrating assets like links and stylesheets in the head. Things that only ran on the server and didn't need to run in the browser.

And also unrelated, working on the Solid integration with Astro, I had added a mechanism to set a prefix for hydration roots to prevent the duplication of these IDs for unrelated islands.

It just never occurred to me that we could feed our own IDs in as the prefix. And since it would just append on the end we could hydrate a Server rendered Solid page starting at any point on the page. With <NoHydration> we could stop hydrating at any point to isolate the children as server-only.


Hybrid Routing

Image description

For all the benefits of Islands and Partial Hydration, to not ship all the JavaScript, you need to not require that code in the browser. The moment you need to client render pages you need all the code to render the next page.

While Technologies like Turbo have been used to fetch and replace the HTML without fully reloading the page, people have noted this often felt clunky.

But we had an idea a while back that we could take our nested routing and only replace HTML partials. Back in March, Ryan Turnquist(co-creator of Solid Router) made this demo. While not much of a visual demo it proved we could have this sort of functionality with only 1.3kb of JavaScript.

The trick was that through event delegation of click events we could trigger a client router without hydrating the page. From there we could use AJAX to request the next page and pass along the previous page and the server would know from the route definition exactly what nested parts of the page it needed to render. With the returned HTML the client-side router could swap in the content.


Completing the Picture

The original demo was rough, but it showed a lot of promise. It was still had the double data problem for server-only content and this was something we needed to address in the core. So we added detection for when a Solid Resource was created under a server-only portion of the page. We knew that if what would trigger the data fetching could only happen on the server there was no need to serialize it all. Islands already serialized their props passed in.

We also took this opportunity to create a mechanism to pass reactive context through hydrate calls allowing Context to work in the browser between Islands seperated by server content.

With those in place, we were ready for the recursive Hackernews comments demo:

But there was one thing we were missing. Swapping HTML was all good for new navigations but what about when you need to refresh part of the page? You wouldn't want to lose client state, input focus etc... Nikhil managed a version that did that. But ultimately we ended up using micromorph a light DOM diff written by Nate Moore (of Astro).

And with that, we have ported the Taste movie app demo in its 13kb of JS glory. (Thanks to a gentle nudge from Addy Osmani, and the great work of Nikhil, David, and several members of the Solid community: dev-rb, Muhammad Zaki, Paolo Ricciuti, and others).

The search page especially shows off reloading without losing client state. As you type the input doesn't lose focus even though it needs to update that whole nested panel.

Solid Movies Demo
And on Github

Just to give you an idea of how absurdly small this is. This is the total JavaScript navigating between two movie listings pages, then navigating into a movie in various frameworks with client-side routing from https://tastejs.com/movies/.

Note: Only the Solid demo is using server rendered partials so it is a bit of an unequal comparison. But the point is to emphasize the difference in size. Other frameworks are working on similar solutions, things like RSCs in Next and Containers in Qwik, but these are the demos that are available today.

Qwik demo was originally part of this but they changed from client navigation(SPA) to server(MPA) which makes it unsuitable for this comparison.


Conclusion

The more apps we build this way, the more excited I am about the technology. It feels like a Single Page App in every way yet it's considerably smaller. Honestly, I surprise myself every time I open the network tab.

We're still working on moving this out of experimental and solidifying the APIs. And there is more room to optimize on the server rendering side, but we think there are all the makings of a new sort of architecture here. And that's pretty cool.

Follow our progress on this feature here.

Top comments (20)

Collapse
 
peerreynders profile image
peerreynders • Edited on

Thanks for clearing that up.

I don't know how many times I've watched the Solid Movies App segment but I wasn't sure I was “getting it”—I had a sense of “islands with dynamically server rendered content” but couldn't quite pin it down if that was the case.

In that regard I suspect that the recent Next.js 13 use of “Server Components” doesn't tell the full RSC story as I was under the impression that in the original December 2020 demo server components were able to send updates even well past the first render into the client's VDOM.

So as I understand it Solid Start's dynamically server rendered islands in the Solid Movies Demo are the functional equivalent to the Server Components in the December 2020 RSC demo—which is why they are being referred to as “Solid Server Components” even though technically Solid's components vanish at runtime.

After the “component (repeated) render function” vs. “component (one time) setup function” confusion I'm afraid you may have set yourself up for having to explain repeatedly that “DOM diffing” doesn't mean that there is a VDOM.

Collapse
 
intermundos profile image
intermundos • Edited on

This look solid :)👏👏👏

Collapse
 
diegochavez profile image
Diego Chavez

I'm really impressed! awesome work @ryansolid
The future of solidjs looks bright, I like how you challenged the status quo of the JavaScript Frameworks! this will bring more attention to the possibilities of shipping less Code to achieve interactive web apps.

Collapse
 
rgolawski profile image
Rafał Goławski

Wow, these numbers are really impressive 👏

Collapse
 
crazytieguy profile image
Yoav Tzfati

This is all really exciting and I love your work!

I noticed that backwards navigation in the movies app takes the same time as new navigation. Is this unavoidable? I feel like the ideal solution would have instant backwards navigation.

Collapse
 
ryansolid profile image
Ryan Carniato

Yeah it is possible. We haven't done any sort of caching here yet. I always consider caching the last level of optimization. This sort of architecture begs for back/forward caching but haven't created a solution for that as of yet.

Collapse
 
olegbask profile image
Oleg Bask

I wasted 3 days experimenting with a similar concept using Astro + HTMX, but I haven't realized that Solid Start will support it out of the box. How far are these feature from being completed? I'm extremely impressed with this work!

Collapse
 
ryansolid profile image
Ryan Carniato

Still a ways out. I am not content with the DX. And Context is still an unsolved problem. It works fine for these simple things, but there are things it doesn't support and it isn't clear why. I think the direction is good and I'm sold on what it gives, but we need to do more here. More than some linter rules etc.. So we need to spend some more time with it.

Collapse
 
cherif_b profile image
Cherif BOUCHELAGHEM

isn't something similar to what Iniertiajs is already doing?
inertiajs.com/routing

Collapse
 
peerreynders profile image
peerreynders

The protocol suggests that inertia.js is used to build fully client side rendered (CSR) UIs without having to define a separate supporting API (which likely would require lots of client side JS).

The Islands Architecture tries to minimize the shipped JavaScript by fully rendering the page on the server side as HTML and only delivering just enough JS to make the “islands” interactive.

Solid Start goes further by letting the client side islands manage content that was originally rendered on the server and even replace that content with server content rendered at a later time (while all client side components are hydrated on initial page load).

So the goal is to render as much as possible on the server so that the JS for rendering that server rendered content doesn't have to be shipped to the browser.

Collapse
 
cherif_b profile image
Cherif BOUCHELAGHEM

Thank you for the explanation and the links, really helpful.

Collapse
 
docweirdo profile image
docweirdo

Thank you for the write up and the detailed explanations.

Something I don't quite understand about nested islands:

Why would you want this? Well, there is a rule with Islands that you cannot import and use Server only components in them.

Am I missing something here or is this not the answer to the question? Or to put it more directly, can you rephrase why we want nested islands?

Collapse
 
ryansolid profile image
Ryan Carniato

You are absolutely right. I've added a bit more of an explanation.

Collapse
 
madza profile image
Madza

Always a solid knowledge 👍✨💯

Collapse
 
alexo382 profile image
Alex O

Whoa, this is great, can't wait to play around with it! Why no Remix demo/comparison tho? :(

Collapse
 
ryansolid profile image
Ryan Carniato • Edited on

I don't know. I just grabbed the demos that were posted officially and the one from Qwik I had seen in a similar comparison. I haven't seen a Remix one as of yet. I checked their Discord and MJ suggested the community should build one, but it seems to confirm that one doesn't exist currently.

Collapse
 
danishsiraj profile image
Danish Siraj

Looks great, definitely worth trying 🤔, great work 👏👏

Collapse
 
josiasds profile image
Josias Schneider

This is amazing! Great work!

Collapse
 
michalczaplinski profile image
Michal Czaplinski • Edited on

Awesome work Ryan!

Could you elaborate a bit on this:

Solid's hydration works off of matching hierarchical IDs to templates instantiated in the DOM. They look something like this:

<div data-hk="0-0-1-0-2" />

What does the order of the numbers in the data-hk attribute stand for? What would the 2 and the 1 in this string correspond to?

Collapse
 
ryansolid profile image
Ryan Carniato

The order they are created. Since JSX can be inserted in any order we needed to keep track of that. We can't rely on the order things appear in the DOM. Template partials might be hydrated in a different order than they appear in the DOM. So presumably if the 2nd template is created first on the server it will be hydrated first in the client so being able to match that up is critical.

And the dashes are component depth. That is a bit arbitrary but since our control flow is components it was the easiest way to isolate. It's possible only Suspense and hydration entries need depth but it was a safer bet right now.

Rust language vs others

Stop by this week's meme thread!