skip to content

Let’s Learn SolidStart

Pushing boundaries is nothing new to the Solid.js team. With SolidStart, they’re aiming to bring fine-grained reactivity to fullstack apps. Ryan Carniato teaches us how it works.

Full Transcript

Click to toggle the visibility of the transcript

Captions provided by White Coat Captioning (https://whitecoatcaptioning.com/). Communication Access Realtime Translation (CART) is provided in order to facilitate communication accessibility and may not be a totally verbatim record of the proceedings.

JASON LENGSTORF: Hello, everyone. And welcome to another episode of Learn With Jason. Today, we are bringing back one of my favorite people in the open source community. We're going to be learning about a newish meta framework and to teach us all about it is just a good friend, Ryan Carniato. How you doing?

RYAN CARNIATO: Hey. How you doing? Doing great myself.

JASON LENGSTORF: It's been quite a while since we've had a chance to talk about software.

RYAN CARNIATO: Yeah, 2021.

JASON LENGSTORF: We are long overdue for a catch up. For folks who aren't familiar with you and your work, can you give us a bit of your background?

RYAN CARNIATO: I don't know  I created SolidJS many, many years ago. That was just kind of the start of my journey into open source. I ended up taking that knowledge, writing articles about what it was like to write JavaScript framework, talk about a lot of topics people don't talk about framework design. I ended up getting hired at eBay.

JASON LENGSTORF: And that one's Marko?

RYAN CARNIATO: Yeah. It is very similar to Astro. It's like Astro's bigger brother. So I ended up in with them and the Quick people and all the people trying to push new stuff forward and next thing you know, I found myself working at Netlify, fulltime on open source, mostly Solid, obviously. Having great experience working, you know, out in the open, constantly streaming, publishing articles, hack and my ideas. Anything to push this stuff forward. I'm very intent on, like, you know, moving things in a way forward that isn't just marketing or whatever. It's important that  yeah, there's different aspects, obviously, to software and different priorities, but the most important thing to me is ensuring when we decide to go at an approach and buyin in an architecture, is it buyable? Does it work? Are there benefits here? I'm big on benchmarking and measuring things and trying to wade my way through the crazy mess that is frontend development. [Laughter].

JASON LENGSTORF: You've had a very substantial impact on the industry because I think, you know, you didn't invent signals, but you were responsible for repopularizing them and after you reintroduced the world to signals, it feels like they then got picked up by just about every popular framework and since then, it seems like the Solid team has sort of been not just working on Solid, but very heavily embedded in working with other teams and we've seen, I think a pretty impressive wave of collaborative innovation happening, where, like, all the different frameworks are moving together, learning lessons and moving them in a way that feels healthy. It doesn't seem like people are doing  like, sometimes it feels like people are competitively trying to copy each other's features and sometimes it feels like everybody's working together to try to make the web together and it feels like the last few years in the framework land, feels like we're all working to make the web better.

RYAN CARNIATO: Definitely. I remember when I entered into this conversation, people had really drawn sides a bit. There was, like, the Vue and Svelte, on one side, and React on the other side and there was this templating and JSX and everyone had been set up in their, like, assumptions of where the world had to go and what the paths forward would be. This is one of my early challenges with Solid, even getting adoption. I came in, well, why can't we do this? People are like, that's JSX, you can't do this with JSX, blah, blah, blah.
Solid came out as this thing that shouldn't exist. By fitting into their camps, had actually assumed it wasn't possible. Right. They kind of  and I feel like once we showed what was possible, it just  it opened up the floodgates in a crazy way, right. One of the things  signals, that you mentioned, a big one. Signals were around back in 2010. There had been advanced over the year. [Indiscernible] really important in that role, as well as SJS.
What we discovered  and largely, this is actually thanks to the creator of SJS, finegrained rendering was a thing. It's not about signals. It's a change detection mechanism. It adds more to your framework. Signals allowed us to delete a bunch of stuff. It allowed us to change the assumptions and once we started down that path, next thing you know, everybody  I mean, it took a long time in JavaScript years. Solid and its predecessor, based on SJS, showed that this approach to rendering was viable. SJS showed the clientside to context a lot of the advanced feature's concurrency. This was the way forward until 2017, let's say.
Now fast forward, eight years, and Vue, with Vapor, Svelte, with Svelte 5, Angular's moving this, Preax, almost over the last couple years, almost everyone has actually moved over, you know, like, it took a long time. It took  it feels for me, it's taken eight years. I evangelized for ages.
To your other point, it's more than just the signals thing. This is part of communication that got us all talking. Everyone needing a meta framework. We've seen, like, bundlers become the next frontier. The problem is, once you bring in SSR, the complication goes through the work. You have to abstract it some way. You have to have better tools to build with. You have to build up those building blocks and 

JASON LENGSTORF: This feels like another space where we kind of saw an introduction of a new concept and then a unification because this is  if I'm remembering correctly, this is where, like, Nitro really kind of came in and said, what if you don't have to rebuild every single part of a framework and have most of this working and Vite. What if you didn't have to build a frontend and you could put these things together.
Also, I did a stream recently on Vinxi, which I believe came out of the Solid community, right?

RYAN CARNIATO: Yeah. No, exactly. It's been three years and it's been a very long journey for me, along this trajectory. And  but, yes, this has been all part of the stages of this, kind of evolution. Vite really kicked it off. When they were like, we are for everyone, we support SSR, it was like having Create React app. Because of some very smart, but somewhat simple things. Like, um...having the plugins work crosssectionally. Instead of  with Webpack, here's a plugin for this environment and here's one for this environment. This is for dev, this is for prod, this is for SSR. Vite was like, no, have a single concern. Single feature and you'll make a singer plugin that works for SSR, works for prod, works for dev. It's a minor thing. I think that's why ejection works and why Vite is such a powerful thing.
I saw Vite and that was finally what tipped it over. I was playing around with stuff and everyone's like, Solid needs a meta framework and I'm like, I don't want to write a framework. You write a framework. [Laughter]. It's very difficult. It was hard enough, as a small team, to maintain the surface area of Solid. It's very key, these days, for adoption, because we have to be right there, in the bundler. Even with Vite and Nitro and Vike is another one. It used to be Vite Plugin SSR. Even in the platforms, they're trying to standardize on these things. WinterCG. There's all these service runtimes. It's like the browsers all over again. You have a spec and everyone has to implement their own version.
We spent years  people hopefully now are passed Internet Explorer, right. It's not all their fault, you know what I mean? It was the wild west. We had all these different browsers and some couldn't keep up. We're in that mess right now with serverless and SSR and, you know, different providers are like, we have  I don't know, PPR, specific only to Vercel. We offer these features. And, it's very difficult to navigate and, like, there's just so many of these facets and at a certain point, you have to go from all of those pieces back to the developer and even for us, I would have loved to have picked up Nitro off the shelf. Good to go. There's even more considerations when you have to take this last step to the developer.
You're like, no, this is still a mess. We haven't actually solved things for them. We're too lowlevel. We're too mechanical. And I  I had a difficult challenge with this with Solid because you got a small ecosystem and let's do SSR. Are we going to sacrifice something for this? I couldn't afford to break the ecosystem apart. You know, you've seen React server components. I couldn't be like, guess what, you guys, half your libraries aren't relevant anymore.

JASON LENGSTORF: I would say React is the only community that survived that. Every other community that's tried that break, it's been the death of that framework.

RYAN CARNIATO: Angular, too, I guess.
It's a very difficult proposition so when I looked at the idea of bringing server to the client, I  I was, like, okay, well, I don't want to change these client libraries, I want to use every single one of them, if possible. Back in 2021, before the Solid 1.0 release, I'd come up this idea. I didn't know what to call them. Doing RPC calls, but I did a proxy thing with RPCs and I actually called them Actions, back then. It wasn't based on, like, forms or anything. I was just trying to think, command action. Sometimes I get hooked on QRS, command query segregation. I need a term for this thing, you know. And I remember thinking Svelte has actions and those are different. Whatever. I'm going to do this thing and I was playing around with these RPC calls and seeing Remix for the first time, with their loaders and actions. Yeah, this is kind of cool. And I fell in love with their optimistic update style. I don't like this being route level. I want to be able to call these functions anywhere.
And, I  it's so funny because I was going to Discord and going, guys, we should do something like this and they're like, oh, you've heard of Blitz. I'm like, yeah, yeah, this is almost it. It's still too much. Like, I want it to just be a function and another core member of the Solid team came to me and he basically built a compiler and then later, a bundler. We went down that road. I don't know if you some of you remember, we tried to push a library that gave what later become server functions to everyone. You write a function and call it everywhere. On the client, it's an RPC call. Codesplitting, it's really awkward when you load the server function on the client side but haven't navigated to it on the server. There's no end point.
We went through a lot of iterations of this. He was like  he was like, I'm going to build my framework manager thing. That's where Vinxi came from. But then all the deployment partners. I don't want do figure out the difference between Fly.io or Netlify or whatever. That's  Daniel kept on telling us  Daniel, from Nux, every time, I'd be like, it's hard to make these things agnostic and you'd see Daniel politely put his hand up in the corner, we're doing this. How do you even do that? That seems so ambitious? We found the JS community and take our multibundler, built on Vite, and bring in Nitro as a way of doing it and that's how Vinxi was formed. Once I saw that, I was like, okay, we need to rewrite SolidStart on this because I can't maintain this mess of stuff I've been doing so far.
In the end, we built, mostly because of the work on Vinxi, we built this agnostic piece that would work for any meta framework. Because again, we built it with Solid in mind, we built it with React in mind. I'm sure you'll have Tanner come on and talk about TanStack Start.

JASON LENGSTORF: And that's built on Vinxi.

RYAN CARNIATO: Yeah. Both of us didn't want to build a meta framework. In the process of doing all this exploration, we stumbled on ideas and patterns that have actually been profoundly impactful on the rest of the ecosystem. Server functions, this predates server from React. In classic Solid way, switched to using server from our original server function approach because React popular and I wanted bundler  Solid had signals before hooks. When hooks came out, we switched to the array destructuring approach. That wasn't as much to align with React. It was brilliant and I wish I had thought of it earlier.
But, same kind of idea and now, we're seeing server functions all across the map, right, which is the kind of modern version, which is a mix between ours and Remix's reloaders. There's so many places to go with this stuff. I'm always working six months ahead. 1.0 out in May. For me, I'm already, like, three months on the other side. I have to kind of go back in time and be like, what was I doing in the spring?
[Laughter]. Yeah, I don't lie. I get a little nervous coming on this show because it's a good test. This is how I can see well we are  like, we do at actually making things intuitive. Great moments come out of it. I don't know if you remember, Jason, that moment in your stream when it was like, oh, you asked a question, how do you global state for Solid? It didn't occur to think of it that way and we lifted the component from inside the component to outside and we had global state. It happened very first time on this show.

JASON LENGSTORF: I love it. I love it. I mean, that's wonderful. I have a question from the audience and then I want to actually jump in and start playing with this stuff. So, you just talked about how you're always working six months ahead. I've seen your working at kind of a furious pace ever since I met you. What are you doing to make sure you don't burn out on this stuff?

RYAN CARNIATO: Yeah, it's tricky. During the middle of SolidStart, I definitely got to points where I was, like, really, um...like, really tired of doing bug fixes every day, working through stuff, not making much progress. The hardest thing about this, as long as things are moving forward, you can stay excited about it. When things inevitably hit those slowdowns, it was tricky  if you've looked at a Solid repo over time, I've tried to keep the issues down. It's larger now. I really took pride in the quality of what we put out there. SolidStart got to 400500 issues on the first beta. I couldn't manage them. [Indiscernible] had started a new job, he wasn't around anymore. At that time, he took three to six months off from open source and I was just, like, looking at the stockpile. There were people helping with issues, but I wasn't getting anywhere.
It was beta, so it was fine. I have 1.1 bankrupt it and it was a rewrite. It is tricky. It's important to have perspective, sometimes that means walking away from the computer. So I think  I think that's key, but I mean, it also helps that I'm incredibly motivated. I don't know where they motivation comes from, all the time, but, like, as long as we can keep on moving things forward, you know, it  you know, I just, like, switch gears. I start writing instead of coding or find some way of just address  like, you know, exercising a different perspective. The best thing to step away from a problem, often not doing anything related. Sometimes these things take time, which has always been the hardest thing for me. I thought if I worked all day I'd get more work done and that wasn't true. You know, thinking ideas only come so quickly. You have to give it time to breathe, to actually, like, understand what you're actually doing. So...yeah.

JASON LENGSTORF: The abstract part of process is very difficult to quantify but, you know, you're exactly right. You can't work your way through the hard problem. You have to do the work you understand and let your subconscious organize pieces otherwise, you're grinding against nothing.

RYAN CARNIATO: There's a followup question about, do you do any sports? Hiking is something I avidly do. That actually helps a lot with thinking. Go hiking with my family, we're actually all avid hikers. We're from British Columbia. We're in San Jose now. It's  it's one of my favorite activities. I think it's really important.

JASON LENGSTORF: I'm not  I don't hike in nature, but I hike in cities. I love a good 20minute walk through the city to go to a coffee shop or to get lunch or something like that. That, to me  I use it for the same thing because I don't have my phone out, I can't look at computer. Walking, head down, sorting through the things that are going on in my life. Yeah, I'm very much a fan of just a walk. A walk is such a good, strategic unlock. Like, it forces you to think instead of do and I really feel that that's undervalued by a lot of folks.
Okay. So, I want to make sure that we have plenty of time for coding so I'm going to bump us over into the pair programming view, so give me just a moment to get my screen shared here...and, hide that thing. First and foremost, we're talking to Ryan. So, make sure that you head over and give Ryan a follow on all the platforms. You can find links up in here, get on that YouTube list, too, because that's a good one.
We're are learning today, thanks to Netlify, who's covering the captioning. We've got Vanessa, here, from White Coat Captioning. Let me put the banner up for those. This is the link, down at the bottom, if you want to get the closed captions for the show today.
We also are going to be working with Tuple, it is for pair programming. We're going to be using it so Ryan can point something on the screen or paste something from his machine to mine so we are able to work faster. If you haven't tried it, give it a try.
We've been talking about SolidJS. Let me throw that in the chat here. It's great. We've got episodes, in the backlog, about Solid.
Specifically, today, we're going to be talking about SolidStart, so let me get the SolidStart link onscreen here.
Ryan, if we want to learn SolidStart, what should we do first?

RYAN CARNIATO: We should get a project doing. I don't know what your preferred place to get your project going. We have a few options here, as you can see.

JASON LENGSTORF: There's a couple of those things down here and we'll get back here and I'm going to  whoops. What do I do? I had this open and then I  [Laughter].

RYAN CARNIATO: We can do  what is it? "PMP Solid." The question is, do we want to do a basic project with the router or a bare project? This is a funny first decision and it says a lot about our  our, um  our whole perspective here. Because SolidStart, at its very base, does not actually need to ship with the router. It's  we've tried to build the meta framework routeragnostic. If you've seen VINXI previously, we built it very much with this in mind. There's a file system routing system but it can feed into any file router.
So, I think  we might install a couple of these templates throughout the course of the stream, just to play around. Maybe we can just start with  let's start with  let's just do basic, why not.
The thing is, the bare template, while very cool that you literally gate an app JS and it's like Create App, back in the day, or how you'd start a typical Vite project, I think most people are probably going to want a router so we can start there.

JASON LENGSTORF: Great. So, I'm running the "npm install here" and I'm going to "get init." And we are going to open this up. And we want  what did I call it? There it is.
All right. So I'm going to maximize this again. And, we are installed, so, let me "npm run dev."

RYAN CARNIATO: Exactly. You can see the Vinxi there. The only difference between this and the bare one is that there's a router, which is why there's links at the top. SolidStart, without the router, I think it bundles to 4.5 of JavaScript. With the router, we're up to about 9 or 10 kilobytes. This is the basic setup for this. It was really important, to me, that if we're going to solve SSR, we're going to solve SSR for everyone who uses Solid, not just for  you know  people who use Solid router or  like, I wanted to make sure, you know  people are working on stuff, like TanStack Start. There's a whole TanStack. I want people to be like, I'm done with React, I'm going to use Solid and be like, oh, yeah, it just works. I don't want them to be like, oh, I have to switch from Next to Remix. None of that. The server functions are a way we can preserve any clientside API. If you have a React query and you want to convert it to Solid query, that's cool. But if you're like, I don't care to make API endpoints, we can call that server function directly.
I think we'll play around with that, to actually show the functionality, in a minute. This is your typical SSR app. Go to appconfig.ts.

JASON LENGSTORF: Oh, wow.

RYAN CARNIATO: Which is very similar to Vite Config. Can you add an option of "SSR false"? Not everyone's ready for their app to be SSR'd. This is now clientrendered. The difference between SSR on and SSR off is a toggle switch.

This has become vogue recently. We learned a lot from Remix. Pulling back in the router is very inspired by us. That's why SolidStart is so lightweight. The router has the opinions. If you have a different router, you can have the opinions and pull those in. I'll show a bit about how that works in a second here.
My point is, right off the bat, you can get that Create Reacttype experience, just by doing SSR false. Now you are back to where you were. You know, this is your simple "hello world" app.
But we're going to do more than that, here today, right? Let's  yeah, I mean, on and off doesn't really matter. We'll do do SSR true. The default's true.

JASON LENGSTORF: Default's true, so we'll leave it on so I can get rid of it entirely.

RYAN CARNIATO: Yeah. And let's look at  let's switch back to our code view here and look at what the project got installed with, just to kind of give you the idea here. So, um, the few files. We looked at App Config. It's interested how the Package JSON 

JASON LENGSTORF: This is a weird thing, they do it when they collapse the file and I haven't bothered figuring out how to turn it off. [Laughter].

RYAN CARNIATO: We have a few entry files. We have  we have entry client, which  which is basically just, like, our client mount point. This is, like, your render call in React. We made a wrapper called Mount, which is basically render or  sorry, render or hydrate, based on what mode you're in. Right. That's literally all it's doing. If you had some specific stuff that you want to only happen in the client, you have to put up a service worker, put it here. It imports "start client," which is on the client side.
If we go to "entry server," it's a server equivalent of it. This is actually kind of like our index HTML. "Start server "gets a document and you write it in JSX, essentially. So, this is where we've diverged a bit. We figured, like, this keeps things closer in, you know, JSX, what people are using. So, this  this is only rendered on the server. This approach of doing this only rendered on the server means that we're going back to that classic entrypoint. You see "div id app." This is like the starter templates of clientonly. Even though this is SSR, you ignore these two files. You go right to app.tsx. I just wanted to get them out of the way because app.tsx is where export default app  you know  this should be fairly familiar to people.
As you see, in this example, we have context providers, suspense, router, Solid router. For most people, the only thing that they'll probably actually ever import from SolidStart is that File Routes, that you see there, which is just a component, which is the route configuration that we get from  from the file system routing, via VINXI. If someone was to say  we can do this exercise, why not. Can you above default  outside of the function and do "consol.log."

JASON LENGSTORF: Does that show up here or in the browser?

RYAN CARNIATO: That should show up in both places, I think. Hopefully if we maybe refresh the page. You know what? It's a toplevel. This might be a place we have to restart the server. On the client, we see it here. Like, this is the  the manifest of the components. That's  and this is Vinxi's file routing system. We parse what we need. For us, our router takes a component prop, which is basically  it's lazyloaded. There's a preload call on it. And then we have info to say it's from the file system. The path of the route, see that slash, if you go down to the  let's pick a different route. You'll see a different path, like, "/404." This should be another one that's about the third route. Right.
So, like, essentially, we just  you can map this to however you want. I didn't remove some of the extra fields on here. Solid's router only cares about children, path and info. It doesn't  we get this metadata from the file system and we could basically choose any file system running convention. It's bit into Vinxi. But it's pretty trivial.
So, this has kind of been our approach. Everyone has opinions about file system routing. It's a pain to get everything aligned. I picked one that I personally like, for various reason, whole different debate. The idea here is all of this stuff is completely configurable.

JASON LENGSTORF: Let me drop a link to this episode because Dev, who's in the chat today, already taught us how Vinxi, itself, works, so we built our own meta framework using it. So if you're interested in that, you'll see a lot of what we're seeing in the console here, will look familiar when you start digging around in Vinxi.
This is an extremely lightweight setup. There is very little that feels custom to SolidStart.

RYAN CARNIATO: It is like route preloading and optimizings. There's a reason every framework has file system routing. I think it's an important piece but the conventions can sway. Picking the right agnostic tools and why we had to create Vinxi. I was telling somebody, the worst part about Vite, for me, is index.html. It is very helpful but it's, like, you can't put it where you want it. It doesn't go in the source folder, it goes toplevel. In general, those type of conventions are the hardest things to fight against, as a framework author. I tried to rebuild SolidStart on Astro, at one point. Big fan of Astro, actually supporting the Astro shirt today.
Trying to find the right tools that step completely out of your way is hard. So, we're trying to respect that the best of we can, as well. Every layer gets harder and harder to do that, so, yeah, but this is the basic thing and it works basically how you would expect. There's an index page and an About page. Like most clientbased apps, it will just  like, the index page is what we've been seeing so far, which is just, like, the counter. Hello World counter and then the About page, I think, is almost identical.

JASON LENGSTORF: Just a heading.

RYAN CARNIATO: Exactly. Right. The whole say that SSR frameworks work is when you load, the server render or server prerender  we support that via an option in the config. Afterwards, once you start navigating around, it's all clientside  you know  navigation and fetching.

JASON LENGSTORF: It's the standard approach that we've sort of  we've come to expect this, right, out of a framework, is that it will load any page from the server, but we expect it to do the singlepage app style navigation once we've loaded it.

RYAN CARNIATO: Right, right. This is no different than with, say, Next Pages Directory.
We've just been working a lot on kind of honing in on making this relation feel a lot cleaner.
And  yeah. So, the  there's  I guess, we  maybe we should just show  before we move on, maybe something we can do is make  show some of the features that SolidStart actually have, itself, that is actually unique. We talked about the file system routing, that's a big one. The other feature, that SolidStart, are these server functions. It might be fun to make a server function, right now, just to kind of get a feel for what it is.
So, how about we  it doesn't really matter where we mount it. We could make a button and have a click do it. Let's just do it.
We can make an async function. Call it whatever you want. "Hello," or something. "Greeting." Right here, put "use server." Yeah, exactly. And, this tells SolidStart that the content of this does not run on the client. So, the classic thing. You consolelog here and what should happen is if we call this function from, say, a button click or something  we can wire that up, we can just add another button maybe. Doesn't really matter  yeah.

JASON LENGSTORF: See how fast I can do this. We'll do  we'll call "hello."

RYAN CARNIATO: Right. We should see the message only show up, um...on the console, on the server, not on the client. Right. And you can see My Secrets over and over again and not there.
This is the most basic example of a server function. You start being able to see incredible power here because, like, can we  can we add something? Can we add an argument? Like, put a name on, like, "hello"? Yeah. Like  yeah.

JASON LENGSTORF: Okay. Um...

RYAN CARNIATO: Now look at what happened over to where we called it. Right. Like, essentially, TypeScript is telling you, you didn't call it. You have type information. This is a big advantage over, say, normal API call. Because there's no extra, like, different signature, like, we have different ways to get the request and stuff. You can basically have these swapin identically for existing APIs. We're doing click handler. Let's put a name in there, doesn't matter. "Hello, Jason." "Hello, Ryan." "Hello, chat." There we go.
This obviously  at this point, I don't feel like is  you know  that special. We've seen similar stuff. But the key point here is that is all this is. This has no other behavior. You can return stuff  that's not true. We have a crazy serializer where we can serialize promises that serialize promises. We have  I mean  I keep  I undersell how powerful this thing is. You can literally return a stream and have it stream the content back over the wire. I'm like, yeah, oh, nothing.

JASON LENGSTORF: I mean, the thing that's interesting to me, from an implementation standpoint, I have a function and, like, these are both declared in the file and just by saying where I want to use it, I'm able to make sure that it works, where I want it to work, right. This is the sort of thing that  this is deceptively hard because what  what we've seen happen, in other frameworks, you have to have a special filenaming convention of .server or .tsx. Or remember what the call chain is because the server function has to be called in a certain way but if there's a call chain, it can break and leak secrets, whereas having things like this, don't run this on the client and then it doesn't run on the client. [Laughter]. Like, it's one of those things that it doesn't seem like a big deal until you realize how hard this is to do. Right. And so I think  yeah, it's easy to undersell because making something simple, it hides the amount of pain that it took to get there. [Laughter].

RYAN CARNIATO: The serialization stuff is really cool, advanced serialization and being able to stream data and do stuff. Which will come in key maybe a little bit later in the demo here. But what's important for us was, this was the only part SolidStart cares about. If you want cool stuff, like, actions, like, Remix does, you basically load data on the page and then invalidate it and then refresh the data and do all that stuff, that's not SolidStart's responsibility. SolidStart goes, oh, if you want to use a server function, you can. Use your exact clientside APIs as they are. The thing is, like  the thing is, with Solid, like, the server function that might not be obvious is you can call these functions as you're rendering on the server, right, and they won't do an RPC call. If you do the function inside of home  let's put "hello, chat" here. Or, "hello, server." Now, if we do this and we refresh the page  I guess we have a bit of a history here  you're going to see, like, "hello, Ryan" initially on server render. If you go to About  stay here. And then go back...you'll see, "hello, Ryan" again because it's 

JASON LENGSTORF: It's being called on the initial server, like, on the call. We get the ability to write an SSR function, so the equivalent to, like, when I'm in Astro or whatever, and I do, like, a "get initial props" in, like, older frameworks. Right. That feel of, I know this is going to execute when the page is initially rendered or when we do static generation, it calls then. Also, I get to use that same function as an onclick handler without changing the functionality and you're changing this under the hood to be the RPC call so that I don't have to think about that at all.

RYAN CARNIATO: Right. And also when you navigate to the same page again, to the client side is what I was trying to get at. If you go back here, see how it's showing "Ryan" again, even though it was rendered initially on the server, because it's part of the clientside render, as well, you can get it on the client every time you go back and forth between  you can hit it on the server every time. The data fetching, essentially, to show the data you need on that page, whether it originated originally on the server or a future client navigation, looks identical.

JASON LENGSTORF: Yeah. Yeah, yeah. I mean, this is great. Like, this feels really nice. There's a good question in the chat: Do you have guardrails for clientexclusive stuff? If I try to call "window.something"?

RYAN CARNIATO: This is where things get a little bit tricky. I mean, it won't be defined so it'll just error. One of the places  it's more the other way that gets a little awkward. It's possible for treeshaking to fail in some cases where the server module is effectful, which means that it basically won't let itself be removed from the package. We aren't doing a bunch of fancy  we did an older version of the plugin. We are doing more aggressive treeshaking. Generallyspeaking, window is going to error, essentially.

JASON LENGSTORF: It'll error entirely?

RYAN CARNIATO: When you runtime it. It'll say it's undefined. Window  yeah, I think the more important thing is that in React server components, they have, um...they have something called, like, serveronly, where you import it and it throws if that piece gets in the client code. I haven't built that. It's literally a oneliner, though. "If not, is server throw," I haven't actually built the package for it. You could build that in very, very quickly and basically you can mark something as serveronly. And then if that ever got in the client build, the client build would basically crash immediately.

JASON LENGSTORF: Can I do something silly, because we're using Vinxi, I can come back out here to my index and I  let's bring back our other function and we'll say  we'll call it, "hello," too, this time. I'm going to get rid of the server and instead, I'm just going to log my secret. We'll do the import.meta.env and...right. And so now when I click this...did I reload? Oh, I have to actually call the function... and it doesn't work. It does prevent me from accidentally leaking my serverside stuff.

RYAN CARNIATO: Because your secrets are runtime and not part of the client build  there are ways that I could think of where you could leak certain things, um...like  like, with Next and other ones using these kind of things, like Remix, there is a potentially way to  to have this stuff happen if you're not paying attention, like, forget to put [Indiscernible] server and then, like  like, a secret just like this automatically has guards because of Vite. But, like  yeah 

JASON LENGSTORF: I also realizing I'm calling something wrong because it's not picking up.

RYAN CARNIATO: Meta stuff has been bugging me again recently. Vite and Nitro has different settings. I wonder if I have to do process.m. because it's running in Node locally. Try 

JASON LENGSTORF: Ah, there it is. And if I do one of these and we go back to here...

RYAN CARNIATO: The client  I don't think [Indiscernible] because it doesn't get in its build.

JASON LENGSTORF: Yeah. It's doing the thing. It's doing what we want. I mean, like, this is supercool that this just works, you know?

RYAN CARNIATO: The reason for this, as I said, is for the transparency. This is all the functionality that it starts. I wanted to do more. I don't think we'll have time to code more, which means I want to jump to another one of the templates. Let's just run the install script and just jump to it.
So...I guess there's two different demos I could show up here. There's one with progressive enhancement and forms  actually, let's do the Notes one. That's the super one that has everything. This demo was created originally by the React team when they wanted to showcase server components. This doesn't change your mental model. Even though the boundaries are more permissive, with these server functions  I don't know if you've been hearing about skew protection on Next.js and Vercel. If you change a component prop, suddenly, like, basically because the boundaries between client and server components is API surface, any, like, code change could cause version mismatches. This could happen, too, with server functions. But generally people define server functions the way they define API functions. There are places where you hit that. Whereas with server components, that can happen, like, anywhere throughout the tree, in just the way that you literally insert it in the DOM tree which means this isn't quite as volatile.
I saw their demo, as usual, what if we made this demo without server components and had, you know, almost all the same benefits? Let's play with it first and I'll show you how it works. Let's make a new note.
So, let's give it a title. There's a Markdown preview editor that we download.

JASON LENGSTORF: Oh, nice.

RYAN CARNIATO: And then when we're ready, we can hit "done." This will update our note here. This is all clientside. Yeah. I mean, we can  what else can we do in this app? We can expand the note on the side. Here...

JASON LENGSTORF: Oh, cool.

RYAN CARNIATO: Yeah. And then we can also, like  yeah. It's a client app so if you expand the note...and then click "new," you know, and add another note, like, we're not  this is a complete client app. We're not losing state. Like, this is  this is  you know. Right. Yeah. Yeah. I mean, if  yeah. If you hide the note, then it will lose the state because it's unmounted. But if the other note that's not hidden will still be expanded, et cetera.
Now, let's look at how the  this is actually implemented. Um...yeah. I mean, I guess I could have make it work in a way that that didn't lose expansion.

JASON LENGSTORF: It's a demo, right?

RYAN CARNIATO: So  this  this app  if we go to app.tsx, it's actually not going to be all that different than the other one, except now, we have a sidebar in our route, here, and it has a search field, um  button. The notes loading have their own suspense boundary around a note list. You see this when you initially load the page. If you refresh the page, you might see the reloading state. No, it's too fast. [Laughter].

JASON LENGSTORF: Let me throttle it.

RYAN CARNIATO: It's also possible that we have this one set on "defer." You have the ability to choose whether the rendering waits on it or whether it  like, by default, we stream. But you have the  you can choose whether the server waits until that piece is loaded to produce loading spinners versus not.

JASON LENGSTORF: It looks like it's set on "defer."

RYAN CARNIATO: We can actually check that in a minute. Either that, or it's really fast because it's local.
The setup for this project is that  is that  yeah. Let's  let's look at a specific route, I think is probably our best bet. Let's go to...notes  inside notes. "Home" is a blank one. "New" is a new one. Let's do Preview. The brackets for us means that that's not there. That's just my convention  I hate people having to write the same file name over and over, like, page.tsx in, like, 50 million folders. I hated it so much that I got rid of index.tsx, as well. When you put parenthesis on the file, that means that's the index route, essentially. There's no path.

JASON LENGSTORF: So effectively, the parenthesis are giving us a chance to label it. But it's not used as routing information.

RYAN CARNIATO: Yeah. Exactly. So that's like your index. So when we go in here, what you're going to see is what most routes look like in SolidStart, which is you export your page. There's a "create async" call, which is where we do our data fetching for the page. And, you'll notice, though, above that, there is this route config that also calls the same function with the parameters. Seems like duplication, but the idea, here, is that you have the ability, optionally  and I think it's a good idea when you have client involved  when hover links go, it will preload that data so that the second  even if the code hasn't loaded for the page in the browser, it starts preloading in parallel the data for that next page.
And, this is why people use things like loaders. But, for us, we  this is  we call it a preloader. And the important thing to notice here is this route config satisfies route definition. That comes from Solid router because we're using Solid router. If we didn't use Solid router, that would be fine, too. It is whatever the route config is for your router, this is part of the router agnostic thing I was talking about before.
It has a preload function. If yours has loader, you can have it satisfy whatever your route is.

JASON LENGSTORF: Oooohhhh. I get it. I get it. I understand.

RYAN CARNIATO: So, this is just our convention, for  for doing preloads. So, quite often, what you'll see is a function being called in the preload and in the component where it's actually used. And, the  the function, itself, "get note preview," if we go "Lib API," what you'll see is functions. The server functions would be fine. We already played with those. They are doing operations off of [Indiscernible] from Unstorage. It's a universal KV. We're using it in a file system storage, in this demo here. But if you can look at all these functions here, they're just using  these are the  these are all the server functions. I might have put "use server" functions. I wanted to add extra behavior that has nothing to do with server functions, themselves, that work universally. You see cache and action wrapping these server functions. Cache is like cache React. You can set a cache see, the second argument, "no preview," you can set a preview. This creates some React invalidation and automatic deduplication on the server. And then the action is  is another one where  if we scroll down just a little bit so we can see the bottom of "save note."

JASON LENGSTORF: Clicked the one button. Here we go. All right. If I scroll down...

RYAN CARNIATO: So we can get the bottom of most of "save note" here. This one's actually using a form data, but  it's actually using a combination of data and form data.
Let's look down at the bottom. Return and redirect. It has routing information. It's based on Remix's approach, but the  you can also invalidate certain cache keys, by default, like Remix. You can actually tell it, say only  the redirect is a response helper that says, "only revalidate this key." This is not a behavior of server functions, it's a behavior of actions. We've basically just taken a very basic concept and given it a whole bunch of superpowers of invalidation and automatic handling, just by wrapping them and  because you scroll at the top, cache and action, import, they 

JASON LENGSTORF: Oh.

RYAN CARNIATO: And redirect. They come from the router, they're not part of SolidStart. If you don't want to use any of this stuff and you want to use TanStack, use it. They have their own cache and mutation. The key part here is, Solid's router is familiar with this, caching is automatically restored. This magic combination means that when you wire stuff up like this, it will automatically serialize and manage SSR. So, when you render it on the server, it'll fetch it on the server and then send that data, along with the stream, along with the client, without it refetching. At some point later, you'll hit these endpoints again and have it all update.

JASON LENGSTORF: Gotcha.

RYAN CARNIATO: These systems just keep on compounding with each other. Like, the "save note" does a redirect to the next page. But because of the preload function, we actually know, while we're doing this action, um, that we're, um...where we're going next.

JASON LENGSTORF: We've got the data here.

RYAN CARNIATO: Well, we got the data, here. But we're not actually passing it through. Generally, with these action loader patterns, you perform the action and then you do a "get" to pull the data out. Same thing with TanStack. You do a mutation, you pull the data back. So, you know, in classic HTML forms, you know, you do a post and then you do the "get" and you get the next page you're going to, like when we're saving our note.
Can we go back to our notes example and can we, like, actually run it in the browser?

JASON LENGSTORF: Oh, oh, I gotcha.

RYAN CARNIATO: I want you to open the Network tab, for me, like, the Dev Tools, for a second. Yeah. Let's get the network going. Sweet. Let's add a new note. Okay. Okay. And, can we change, actually, the filter to "fetch" let's add one more note and click "done."

JASON LENGSTORF: Okay. So, it's in here.

RYAN CARNIATO: Now, when you hovered it, another one came. Scroll up to the first request you did, the post. Look at the response, for a second. I know it's a little bit technical, but I wanted people to see this. It didn't just do the post, um  see, this is showing our advanced serialization. We're serializing promises across the wire. It fetched the data for the next page while you were still on the previous page on the client. Most libraries, what would happen is you would post your data and it would come back and the client would be like, oh, sweet, it received it. Next page, load my data for the page. If you remember, we had those preloader functions, on the server, we can do the load. We can tell the client, hey, I'm going to redirect. While it's in flight, we can start preloading the data for the next page. Instead of doing two requests for a post, we can do two. This is like with server actions. We bring this advantage straight to client frameworks. This single fetch move works if SSR's off.

JASON LENGSTORF: Fascinating. Okay.

RYAN CARNIATO: We remove the waterfall on mutation that exists in all singlepage apps. I was like, we can do this without actually changing anything. We already know this information. This is taking your routing information and cache information and improve the time. When you do a mutation, you could always return that data and update the stuff yourself. Right. But what Remix showed us is people hate managing caches. Like, the reason Apollo was so hard was because of the cache management.

JASON LENGSTORF: I was literally about to cite Apollo because I remember writing the mutations with optimistic updates and if you screw it up, the whole thing breaks and it was like, oh, my god. The fact that you've sort of abstracted all of this into, it just works, is really nice and knowing that when I'm looking at my code, here, what I have to get right, for this to function, is, I have to remember my cache keys. And if I do this, everything else works.

RYAN CARNIATO: You treat it like you treat TanStack query. You typically have to do a waterfall because you're on the client and you have to do my mutation, check; do my validation. But we can just do it in one step.

JASON LENGSTORF: Yeah, this is  this is slick. And so you're saying, too, with the way that SolidStart is set up, if I'm using TanStack query, it will solve this problem for TanStack query, as well, or this is unique to your router?

RYAN CARNIATO: This piece is unique to ours. I'm thinking about putting a protocol out there. The difference between preload functions and Remix loaders, they load one thing. Our preloaders run on the client and the server. We can do stuff like  this is where the  this is where I started really diverging from server components. Say you have five different data sources on your page and you perform an action, what happens with server components, you have to rerender the whole page. It rerenders everything. You have to fetch all the data, even the stuff that didn't update in the case. You put a cache. This is why Next is so crazy about caching. The performance would go to the pits if you have to fetch every single thing over again, but that cache has to live on the server because the server is the only thing that can know because you have to rerender it.
You get in there and you have a couple layers of nesting and five sources here, ten sources here. It will be like, yes, those two things need to update, the other eight don't. But it still rerenders the whole pages and serializes the data and  I don't know if you costs any money, you needed to have a KV.
What's really cool about this approach is it's like the cache still lives inside of the client. It's outside of the use server. It's like our clientside routing approaches, where you don't need any KV on the server because the client basically knows what's cached so it  and the server  and because the action can be finegrained to exactly what needs to be invalidated, you can say, oh, yeah, I want to invalidate those two things. If the next thing has data that's not on the next page, we do that. Pretend you have to fetch three new things, you will get the two things back you invalidated and those things. We use a combination of what page you're on to fetch only the data that's updated.

JASON LENGSTORF: This is one of those problems. I remember, back in the  almost 10 years ago now, when I was working at IBM, and first introducing GraphQL, this was a problem. When I moved over to Gatsby, this was a problem. At Netlify, we were trying to figure out, how do we do good cache validation that doesn't become a huge foot gun for people and understand how all the data across their entire app is interconnected and we never found a solution that didn't mean full control, endtoend. And what's really nice here is that you have that control.

RYAN CARNIATO: No, yeah, exactly. That's what we'd been missing. All you have to do is use server. Like, that's the extent of you buying into it, is deciding that you're going to use server functions. Now we can control serialization and deserialization. What's beautiful  you mentioned those places have primitives. Netlify has great primitive caches. This works really well with Netlify's CDN. If you set up your data to be finegrained, like I'm mentioning here, where you have the separate server functions or pieces, then you can cache those requests, technically, and in the same places where we say, hey, invalidate this specific  only this data here, you can say, actually, invalidate through the CDN for this specific data here and it's the same game again.
Because we use responses. Return/redirect. We picked that piece up from Remix and, like, it's very brilliant to use web standards because at the same time, solved the problem that has never really been solved cleanly for singlepage apps. So, we've removed all the waterfalls.
Me and some guy from the React team will argue about waterfalls for all time. We solved it, as well, if not considerably better than  they don't need [Indiscernible] for this. It kind of uses web standards.

JASON LENGSTORF: This is the part where I feel like this is the hill I'm going to die on, is if you solve a problem by inventing a new standard that everybody has to adopt in order to solve the problem, you're fighting a losing battle because you're asking everybody to change everything in order to match the new way of doing things, right; whereas if you build on top of existing standards, people are already doing most of what they need to do to get the benefit of your thing some they don't have to, like, do major rewrites to their code. They don't have to make these giant shifts in their mental models. They don't have to retrain every new team members to understand how the new code works. They just get to keep using the knowledge they already have and it gets more useful because the thing they're doing got smarter because it's built on web standards.

RYAN CARNIATO: Yeah, no, it's great. Okay, Jason, I want to do one more demo. Can we install the classic todo NVC demo here? Let's do one more repo pulldown because I got one more feature to show you. I think we have the best optimistic updates. I want to showcase that off.

JASON LENGSTORF: And then we're going to go into here. I'm going to "npm install."

RYAN CARNIATO: And  yeah. Sorry, I haven't been keeping up with chat. I hope chat's following along.

JASON LENGSTORF: Everybody's been keeping up. There has been side chats about nesting stuff. Y'all, keep in mind, we got about nine minutes left so we're going to look at this and if anybody's got questions, you can drop them in. We'll try to either get them or Ryan can take them for his next stream. [Laughter].

RYAN CARNIATO: I just want to show this one off. You could play with it for a second, if you want to, just to kind of show the functionality. But this is your typical todo [Indiscernible] example.

JASON LENGSTORF: I'm going to open it up, so people can see what it looks like. Wait, I'm in the wrong route.

RYAN CARNIATO: Add a couple [Indiscernible]. You see the purple? That's actually optimistic updates. It's hard to see.

JASON LENGSTORF: It happens fast.

RYAN CARNIATO: Yeah. There's  there's optimistic updates in here. These are all 

JASON LENGSTORF: You can see the color change.

RYAN CARNIATO: These are all singleflight mutations, as well. You know that trick I showed you earlier, it doesn't do two. On the Network tab  we don't have to do that again. You'll see that when we do the mutation, it'll actually return the list of todos again. See, like  there's the todos coming back, like, the refresh. If you have multiple data sources. SolidStart diffs the data. No component's rerun, but then just the single, like, piece  signal updates. Oh, now it's checked. Single update.
If you look at the code, the app is in this one file. And, from Lib API, I made a bunch of server functions and actions, right. They're going to look just like we do in the Notes app because they're using the same KV storage from Unstorage. You could pretend this is your SQL for all intents and purposes. They're use server and they're wrapped into cache some action.
If you go into your index route, you're going to see some code, here, and what's  what we've done here, for the actions, is we wanted to have that optimistic state so let's just pause here. You can basically  the way actions work in Solid is that there's an action, toggle to do or edit to do, you can import it to any component on your page. I heard something where you can only use the submissions in [Indiscernible]. You can have something in the top corner, like, a cart. This is a very common problem. And you can just have the same action basically being read way out of scope because I'm assuming that from your input, you can identify that you [Indiscernible] get me the action of type toggle todo that matches the idea of the current list item that I'm looking at.

JASON LENGSTORF: Got it.

RYAN CARNIATO: And you might be going, for toggle todo, what is that list of inputs? Well, as it would turn out, as we scroll down a little bit...to seeing where we use toggle todo. It's toggle todo with todo ID. I don't like Clojure abstraction. If you want to bind something to your action, we use a "with" command. Within this scope, you're saying, call this action, with this ID and all the form fields and the first argument will be the ID and then the next  the next argument will be the form data and so this is a way of, like, keying the action, perrow, here. Sorry. Let's scroll up a little bit. Actually, I can probably help  no, you scroll up a little bit.
What you can see here is we can use this submission, while it's in flight, to make smart state. Am I pending? Or editingtodo is pending? There's three different submissions in flight here. Am I completed? Again, you end up combining the states of all these inflight submissions. I think the easiest one to see might be, if we scroll down  I ported this example from Remix. There is a second "for" loop under the Main "for" loop. This is where we're showing all the to dos that are in flight, that haven't been submitted yet.
It's a more simplified version. Above are the ones that have already been submitted and tease are the optimistic todos that are in flight.

JASON LENGSTORF: Ooohhhh. So if I go in here and, like, throttle.

RYAN CARNIATO: Or set the delay long. But, yeah, let's throttle. See, it's purple.

JASON LENGSTORF: I got to go faster. Yeah. Yeah. Look at that! Oh, I get it. Okay. Okay. And that is not wild to think about, like, this is a  this makes a lot of sense to me because we're just pulling anything out of  if it's pending, it goes into this adding todo.

RYAN CARNIATO: Exactly. We have use submission and use submissions. So you can choose whether you want it to be a list optimistic or a single one. In this case, it's a list. It's very easy to make a list and show it inflight. Remaining count. There's some fun calculations you have to do when you do this. It's todo's lengths, plus the length, minus the completed length. It took me a minute to figure out how to combine the state. But this means the optimistic derived values also reflect  so while it's pending/adding, you update the counts. You actually see the remaining count goes up, optimistically.
And, yeah, I thought Remix was really genius for kind of coming up with these kind of things because no cache writing here. If something errors, it just stays there as part of the optimistic check. You have these submissions, where you can access basically anywhere and then update the UI and between this and singleflight mutations and all these pieces, the extent of your cache mentality is basically TanStack, like, query. You're like, I have some keys. You're never manually writing to the cache.
Very rarely. There's APIs, lowlevel. This is the developer experience that I was going through. Singleflight mutations, global optimistic updates. Et cetera. This is a crazy thing about SolidStart. I can start at a specific place and go really deep on any specific thing because these aren't all SolidStart features. Your submission comes from the router. Like, this is just how I chose to use SolidStart. Right.

JASON LENGSTORF: I get you. Yeah. Well, I mean, this is really slick. Yeah, I feel like we could look at this stuff all day. But let's  let's  we have just a couple of minutes left  actually, we are out of time. Oh, wow. So, if people want to learn more  I'm going to direct to SolidStart's website. I'm going to direct to the  to your  your links. Where else should people go if they want to learn more about this?

RYAN CARNIATO: Honestly, if they go to Solid website, you will have access to SolidStart docs, et cetera, I believe. We should have access to the docs. Right now, our unified docs  if you click on "documentation," it's all right there. We have a link to our Discord and, yeah. Everything is there. Between GitHub, the site and Discord, you will find us.

JASON LENGSTORF: Awesome. Well, thank you so much, Ryan. And a quick shoutout to Netlify for covering the captioning. Thank you to Vanessa, from White Coat Captioning, for being here today and thank you to Tuple for the ability share screens.
Make sure you keep your eye on this page, here, you can get on the newsletter if you want to stay updated. You can get into my Discord if you want. We have a whole lot of episodes, including, we're going to have Waku come on. I'm not going to try to remember this list. Keep your eye, I'll have the schedule updated.
Ryan, thank you so much for taking some time with us today. This was so much fun. Any parting words, before we wrap this up?

RYAN CARNIATO: Honestly, just keep building and don't be afraid to try stuff because you never know where you're going to end up.

JASON LENGSTORF: On that. We're going to end it. We'll see you next time.

Learn With Jason is made possible by our sponsors: