Loading video player...
If you're tired of Nex.js's confusing use of directives and constantly changing of how caching works, then this video is perfect for you because I'm going to be covering Tanstack Start by building out this full project. I know it may look like a simple to-do list, but this actually covers every single feature in Tanstack Start, including some of the more advanced ones. We have full database support. We have search pram support. We have the ability to edit things with dynamic parameters. We
have delete support. We even have the ability to do different things on the client. For example, this state right here is stored specifically on the client, while everything else is stored on a database on the server. So, I'm going to show you how all of that works, how client server workflow works, and everything else you need to know about Tanstack Start. But most importantly, I'm not just going to tell you what code you need to write to create a project like this. I'm actually going to be
explaining every step of the way what each line of code does, as well as the important concepts you need to understand so you can start building Tanstack start projects with confidence. [music] Welcome back to WebDev Simplified. My name is Kyle and hopefully by the end of this video we'll have this to-do completely checked off. Now to create our very own Tanstack start project, the easiest way to do it is to use the command line tool npm create at tanstack start at latest. This is just going to
use the latest version of tanstack start. And you can see we can give our project a name. If you just use period, it'll create it in the current directory. We're going to use tailwind CSS. You can choose your llinter. We're just going to choose eslint. And then we can choose what add-ons we want, which I think is really cool because this has really all the common things you're going to want to use. For example, if we go down here quite a ways, we can find that we have Drizzle. For our database,
we're going to be using that. Also, you can see that we have Shad CN. And if we wanted things like T3 environment variables, which I would include as well as like Tanstack form, we can include those as well. But for this very simple project, we're just going to be using Shad CN and we're going to be using Drizzle. There's also an example we can install, but we're just going to be using our own base version and we're going to use Postgress. So, it's going to go through and download all the
different files that we need. And while that's happening, I kind of want to explain what each of these files on the lefth hand side is. So if we take a look at the actual generated code, it's very similar to what you would get from like a V project or create react app or next.js application. You can see we have all of like our config files inside of this section. The ones that you may not be super familiar with is thiscta.json. This is essentially just a config file
specifically for tanstack start. I think it stands for create tanstack application.json. Not 100% sure though. But as you can see here, it has things like what add-ons we decided to choose, what our package manager is, all the different things that we selected while going through our project. So you don't really have to worry about this file at all. Other than that, everything else out higher here. You can see this is for shaden. We have Vit because this is built on top of V. So it has all the
different plugins you need. But really all the important code is going to be inside this source section right here. Now the very first file I want to start with is this router.tsx. You'll notice we actually immediately get a TypeScript error inside of this file. And that's because there's this thing called this route tree.gen. This is a file that is generated based on the routes you have defined inside this routes folder. But it's only generated while you're running
your application. So we need to actually do npm rundev. That's where you start up your application. And once we start up our application, you'll actually see that this file is going to be populated and generated with all the different routes in our application. So everything inside of here is going to be generated in this file. And that's what gives us the really good type safety throughout our entire application. Now I just gave that a second and you can see it started
up localhost 3001. If we open it up, we can get the demo on the right, which really doesn't matter. But if we look on the left, you can see we get that route tree. And inside of here is all the generated TypeScript and code that we need to be able to have that type safety throughout our entire application. And the nice thing is is you don't need to mess with this file at all. It's automatically generated for you. And you can see inside of here, we still have that TypeScript error. If we just hit
control shiftp and we restart our TypeScript server, this is going to fix that problem for you. And this may be something that you see the very first time that you create your tanstack start application. If you open this file before you actually generate your root tree. But once we have that done, you can see we no longer have any errors. And now we have full type safety. So, for example, in our application, we have a bunch of these different demo routes inside of our page. And we also have our
homepage. And if we come into our homepage, and I want to just render out like a link, for example, to somewhere. I'll just come in here. I'll render out a link, which is using that Tanstack React router component. It has a two property. And this is a fully type- safe property. You can see we get all of the different routes in our application. And it's fully type safe. If any of these have dynamic parameters or search params or other things that are forced to be
required to be passed along, it's going to ask us for them when we pass it along here. But if we just create a link, for example, test like that, we have now created a link that goes directly to that page. It's up here. And if I click on it, it just redirects me back to the homepage. Now, in our particular application, we don't really care about a lot of the stuff inside of here. So, I'm going to go through and I'm going to delete a lot of the starting code. For
example, all of this stuff inside of here. We'll just return null instead. So, we have a completely blank page. Also, we don't need all of this code related to these features. So, let's get rid of that. And we can get rid of all of these imports that we don't need as well. So, we're just left with essentially the boilerplate code. Now, if we go back into this demo folder, I'm just going to delete the entire thing since we don't need it. And we can get rid of this logo SVG as well. And now
we're just left with an index page, which is like the starting route for our entire application. And we're left with this underscore root.tsx. And this is essentially like the base layout route for our entire application. As you can see, this defines things like our HTML, our body, our header. And the important things inside of here is if we look at the code, we have a head which contains our head content. This is a component directly from Tanstack. We also have down here at the bottom a scripts
component again built into tanstack. So whenever you set this up you need to make sure that you have the head content in the head and the scripts at the bottom. If we remove one of these things inside of our application are going to break. So just make sure that both of those are there. Really the main bulk of our application is where this children is right here. That's where it's going to render out our different routes. So if you're familiar with something like
Nex.js, this is the same exact thing. We have our children inside of our layout and then we have our actual route here which would be like the page.tsx file inside of Nex.js. that's going to render out whatever our different route is. And if we just come in here, we put an H1 that says hi, and we give that a save, we can see that that text high is being printed out on the side of our screen. Now, going back for a second over into this, we can get rid of this header
right here because this is something that's again automatically generated for us when we set up our application. And inside components, you can see that there's this header component. We're just going to completely delete that from here. Same thing with this data. We don't need any of the data inside here, but we do have our database, which is quite nice. This was part of that Drizzle schema setup. You can see here we have a schema right now. We'll modify that to be what we want. And then we
also have an index .ts that essentially hooks all of that data up for us. So we have a database URL that's going to point towards our database in our environment file. So if we open that up, you can see we have that right here. And then we also inside this section have our schema being hooked up right there. So essentially this just hooks up Drizzle for us, which is really nice. I even believe it actually sets up yes commands for us. So we have generate, migrate, push, pull, and studio. So we
can do all of our different database related stuff. That's all part of how we set that up with Drizzle to begin with. Now, while we're on kind of concept here of understanding the different drizzle related stuff, I want to go back into that schema file and just paste in the schema we're going to be using for our application. So, if we take a look at this schema here, you can see it's relatively straightforward everything that's going on. You can see we have our
UU ID, we have a text column with name, we have is complete, we have created at, and we have updated at. So, essentially, we have an ID, we have a name, we have an is completed, and then created at and updated at. And this is for our to-do table. So, we can mostly just keep track of the name and the is completed. Those are really the only things that we care about. Now, it looks like for some reason that autocomplete is not coming through perfectly fine. If I hover over
this error, it just has to do with alphabetical sorting. I'm not too worried about that. We're just going to leave that error in as that is. If you look inside the ESLint, there's tanstack config contains that sorting for you. So, we could go ahead and fix that or deal with that. But, for now, I'm not going to really worry about that because it just our application works. It's just that sorting issue that's in there. So, now that we have that done, let's go
ahead and actually generate and migrate our code. So, we can come into here, we can say npm run db migrate. That's going to create a database migration for us. And of course, we have an error because we don't have all of our Postgress stuff set up correctly because our Postgress is not actually running. Let's go ahead and take a look at our Drizzle config. Looks like we need to make that just an exclamation point to say that it is required that we pass it along. If we
look inside here, we do have our database URL. I'm going to go ahead and change some of the properties in here. For example, I'll change that to Postgress. This will be localhost 5432. And we'll just say like to-do test. Now, let's try to run that again to see if it's going to work. npm run db migrate. And it looks like it's still not quite working properly. I believe there may be a problem with how this config is actually being loaded. I actually don't use when I'm doing my configuration and
I usually just use aenv file instead ofv local. That should actually hopefully fix our problem. So let's go ahead and change that. Then inside of our code we can just get rid of that dependency forv we don't need it. So wherever env is at inside of here we can completely remove that. And then same thing up here where we have our index. We don't need to worry about this config portion as well. So hopefully by doing that we actually fixed our problem. So let's go ahead db
migrate. And it looks like we are still getting an error. Of course, the reason we're getting error is because we first need to run the generate command before we can actually migrate. So, let's go ahead and do generate, which is the command I should have ran to actually begin with. And that, you can see inside here has created a SQL generation for us. And now we can actually run the migrate command. But when I do that, it's not going to work because there is no database being pointed to. It's
trying to find our database, but it can't because it's not currently running. Now, I'm going to be using Docker to set up our database. So, we can just close this. Close this. I'm going to paste in a Docker compose file. So, we take a look at this Docker Compose file. It's pulling down Postgress version 17 and it's hooking up environment variables for our host port, password, username, and name. So, all we need to do is just add those into our environment variables to hook this up
with our database. Now, you could use any type of database you want. You could host it online. You could host it with like PG admin or anything else. I'm just doing it with a Docker because it's incredibly easy to set up. And you can just go onto my GitHub where this is linked. Copy this file directly into your repository. And as long as you have Docker running, it'll work just fine. Now, I'm going to paste down those configuration variables. You can see our
password is the same. That username is the same as up here. Loc host, same exact port, same exact database name. All that should match up one to one. So now all we need to do is just run docker compose up-d and that's going to start this up for us in the background. Now it looked like this failed because I actually was already running a different database on that port. So I closed down that database. We'll rerun this and you can see it started back up. You just need to make sure you have Docker
Desktop installed if you're on like a Windows machine like I am to get Docker working. But again, you can host your database however you want. That's not an important part of this actual tutorial. But now we have a database up and running. We can run npm run db studio to view that database. And I just want to add a simple fake piece of information inside of here. So we can just open this up real quick. Zoom this in. Maybe not that much. Make this quite a bit larger
so it's easier to see. And inside of our schema here, looks like we don't have anything. And that's because we should probably push our changes first. So mpm run db push. That's the same thing as migrate, but we're just directly pushing up our changes with no migration at all. And now when we run this, we should hopefully see our table directly inside of here. So we have our to-dos table inside of here. I just want to add a brand new record which a name that's
just going to say test and we'll just set is complete is going to be false. Let's save those changes. So just so we have a piece of data inside of our application to work with. And now we want to really get into the nitty-gritty of how this works. So let's close out of Drizzle Studio. We no longer need that. And we can just say npm rundev to start up that development server. So we can actually have our application on the right hand side of our screen running. Now what I want to do is go into our
source, go down into our route section cuz that's where all of our routes are going to be defined. And inside this index.tsx tsx route, which essentially, if you're used to Nex.js, is the same as calling this page.tsx. It essentially points to whatever the name of the parent folder is. In our case, we're at the root folder. So, this is just the route of slash or the home route inside of our application. Now, you may notice at the top of the page, we have this
fancy section for create file route with a bunch of stuff going on. And this is kind of how we get all of our type safety in our application. This create file route takes in a path. And this path just matches whatever the path of your files is inside your folder. And this is automatically generated for you. And if I type something in incorrectly, it will actually give me a TypeScript error telling me, hey, this has to be the exact route that I'm pointing to
inside my application. And if I were to create a new folder, for example, test. And inside of here, I create an index.tsx. You can see when I create that, it automatically generates this code for me and paste in the exact URL that I need for the path up here. So, the nice thing is that everything for this is automatically generated for me, which is incredibly nice. There's no extensions or anything you need. This is just all done while the development server is running. It generates all that
code for you, which is a really cool feature. Now, the important thing inside of here is this options argument takes a lot of different things. The first thing you always need to include is a component, which is essentially saying, what do I render at this route? So, we're saying render the app component at this homepage route. But we can pass in a ton of different things. As you can see, the most important one that you're probably going to deal with is loading
data. So, we can pass in a loader, which allows us to load data. And this is a function that runs most importantly not just on the server but also on the client to load whatever data you need. So if we just come in here with our loader and we just say console.log loading data and then we come and we just return something. For example, we just return an object that says name is test. Now essentially it's going to run this code not only on the server but also on the client. Now to access this
information is actually incredibly easy. We have this route object right here which contains everything we need. For example, we can say route dot and we have this use loader data on here. This is going to return to us whatever this loader returns and it's fully types saved which is one of the amazing parts about this. So we can say con data equals and if I hover over this you can see it's a name that is a string and I can just append that data.name onto here
and now you can see if I just give it a quick refresh you can see it says hi and then test which is the name that I passed in. If I pass in a different name and I refresh you can see that name changes over here. Now you will notice when I change for example my loader and I save it doesn't automatically refresh on the right hand side of my page. I need to manually do it. That's because essentially we're changing how the data for our page is loaded but our page is
already loaded. So it doesn't refresh this data. If I change code down here and I save it automatically refreshes. But if you're changing like the loading data and you don't reload your page, it's never going to run. This is something that I guess might be changed in the future, but currently as it stands, you just need to refresh when you change things related to loading. Now you may think, okay, I have a loader. I'm just going to throw my database code directly in there. But
again, like I said, this runs on the client and it runs on the server. So, we're going to run into some weird problems when we do that. Let's go ahead and actually just try this. We'll just do a quick database. Make sure we get our database dot and we want to do a quick query on the to-dos table. And let's just say that we want to find all of them by doing find many. And we'll just return that data just like that. So now this is going to give me essentially all of my to-dos. So I can say here
to-dos just like that. If we hover over this, you can see I get the full type for what that to-do should be. And then I could render that out. For example, I could say to-dos.length. And now you can see we get one being printed out because we have one to-do currently being rendered. Now, it may seem like this is working just like we expect because when I refresh my page, I don't get any errors. And if I go into my console over here, you can see I get some errors, but
it doesn't really look like anything's happening. It's mostly just some extensions I have installed. If I disable those and bring this back over, I still get some errors. But still, it's a little bit confusing exactly what's going on. Like none of these errors really make sense if you read through them and try to figure out class extends. Like what the heck is that even talking about? If you look down here though, you can see it's trying to load that Postgress library and that's
because this database code is being run on the server and on the client and the client doesn't know how to run this because it involves a bunch of Postgress stuff and other things that it doesn't have access to. This was one of the things that really confused me when I first got started with Tanstack start was because when I was used to Nex.js, I had React server components where I could run my code on the server and it ran on the server and it never actually ran on the client. But with Tanstack
Start, everything inside of loader runs both on the client and on the server. So like when I navigate to a new page, it just calls this lo loader directly from the client to get what my new data is. So this is where we need to use what's called a server function, which allows us to call code from the client that actually runs on the server. You can think of it just like a server action inside of Nex.js. But these server functions aren't just for posting data,
they're also for getting data and doing other things as well. So we can run create server function just like that. And this create server function takes in a few things. The first thing it takes in is what type of method do we want? Is this a get or a post? Like I said, we can do both with these server functions. In our case, we're trying to get data because we want to get all these different to-dos. So, we'll say.get. Then what we can do is we can either if we have input data, use an input
validator or in our case, since we have no inputs to our function. You can see our function does not take any inputs. We can use the handler instead, which just takes in a function. So, we can take in whatever this function is, and we can run our code. So, in here, we're just going to return database.query and get all of our different to-do information from here. This is going to return to us a variable which we can just call whatever we want. I'm just going to call it server function or
server loader. Doesn't really matter. So now what I can do instead of calling the code like I was before, I can call my server loader. So really all I've done is I've wrapped this inside of essentially a server function. Now if I give that a quick save and I refresh and I inspect my page and I look at my console, you can see all of those errors have been removed because we're no longer trying to load that code on the client. Instead, what happening was I create a server function is when it
needs to get that data, it makes a fetch request using the get method and it runs this code on the server and then this returns the code down to the client for me. So by wrapping everything in this create server function, I'm essentially telling the browser, hey, make a fetch request if we're on the client and if you're on the server, just call the code exactly as is with no changes at all. So this allows me to call the same function both on the client and the server and
it'll automatically handle calling the server and doing all the fetch requests for me behind the scenes. Again, if you're used to working with the server actions inside of Nex.js, this is essentially the exact same thing but inside of Tanstack and it works not only for posting data but also for getting data which is really important. So now essentially all we have to do is to create the layout for our homepage to see what that looks like cuz right now just this H1 does not look great. So
let's come in here with a div. We're going to put a minimum height of screen. Use the class of container. And then we're going to space things out in the Y direction by eight. There we go. I also want to modify what my container is like inside of Tailwind. So, we can go into our styles. I'm just going to scroll all the way to the very bottom. I want to have a utility here for container. And I just want to apply a few styles here. The first is I'm going to give it a
maximum width. I'm going to make it so it's centered on the left and the right. And I'm also going to give it a little bit of padding just to space that element out. So now if I just put some text inside of here, you can see we got a little bit of space on the left and the right of all my different sides. Next thing I want to do directly inside of here is to put my header information. So this is going to be flex. It's going to be justify with some space between
our items are going to be in the center and we're going to use a gap of four just like that. Then inside of here I'm going to have essentially the lefth hand section that title. So I'm going to say div with a class name of space y 2. There we go. And inside here, I essentially want to render out my title, which is just going to be to-do list with a large text size and font bold. And then I want to render out some content inside of a badge on how many of my to-dos I've completed out of the
total number of to-dos. To do this, I just need to load some data all the way at the top here. So I can just say const completed count is going to be to-dos. And I want to filter only on the to-dos that are completed. And I want to get the length of that. And then to get the total count, I can say total count is just my to-dos.length. length. There we go. Now, we need to get a badge component from shad CN. So, let's come over here and install that. npx shad cn
at latest add. And we want to add the badge. We might as well add in some other components such as the button and the table component while we're at it cuz we're going to be using all of those different components. Now, we can just give that a second to install in the background for us. And that'll give us this badge component right here. That just finished installing. So, let's go ahead and add in that badge. And we're going to make sure we put our equal sign
up here. Now, if I give that a quick save and refresh, you'll notice that it looks like a failed to fetch dynamically imported module blah blah blah blah blah. Let's just give it a refresh to see if that's still happening. So, sometimes when you get these types of errors, one of the best things you can do is you just close your application completely and restarted. Tanstack start is still in quite early days. So, sometimes you get these weird bugs that happen. So, if we just restart our
application, you can see here now everything is working like before. I want to put this into dark mode. So, inside of our route for our route here, I'm just going to add a class name of dark just like that. So, now we're in dark mode. You can see to list as well as the amount of to-dos that we've actually completed. Now, on the right hand side of all this content, I also want to add a section for our buttons. And the main button I want to have is going to be a button for adding a new
to-do. So, we're going to come in here. This button is going to contain a link that is going to go to a specific page, which is /todos/new. And we'll just say here, add to-do. And we'll put the plus icon inside of there. And this plus icon is not importing for some reason. So, we'll just do a quick import up here of plus icon from Lucid React. Just like that. So, now if we scroll down, you can see that that plus icon is showing up. We obviously need to style this a little bit. And we need to
make sure that it is an as child. And actually, for our styles, all I really want to change is the size. I'm just going to make it a smaller button. There we go. And if we expand our screen a little bit, you can kind of see how this style is looking. But obviously, when we go to this page, we're going to get a big not found because we don't actually have a page there. And you can even see we're getting an error cuz we don't have a page there yet. So, inside of our
routes, let's create a brand new folder. We'll call this to-dos. Inside here, we'll create a folder called new. And then inside of there, we'll create our index.tsx. That's going to automatically generate our code. And you can see it automatically refreshed over here. Now, the really nice thing is we can actually generate this file in essentially the exact same way by using a slightly different format. So, instead of using folders, you can actually use files
instead. So, here I could just create a file that was called new instead. So, I could say new.tsx. And that new.tsx file is exactly the same. And if I put that inside that to-dos folder and I get rid of this index, you can see it's rendering the exact same file. If I just give it a name that just says hello, you can see that's what's rendering at the exact same route. So, we can use the name of the file if we want or we can use index or we can even go a step further and I
can create a file called to-dos new.tsx. And that dot syntax essentially acts the exact same way. So now if I just get rid of this entire to-do folder, you can see it's rendering the code directly inside of here. I'll even delete here so you can see it's rendering this code. So depending on your preferences, you can use the dot syntax, you can use the folder syntax, or a combination of them. Now, in my particular scenario, I kind of like using the folder syntax. So,
we're going to stick with folders where we have to-dos new. And inside of there, we have our index.tsx. If I just give that a quick refresh and make sure that I probably have to restart my application just because it's a little bit confused since I undid some stuff. There we go. You can now see that that is working just fine. Now, before we work on this page, I do want to finish up what this page right here for our actual list is going to look like. So, we have our entire header section done.
So, we can minimize that header section. And below this, we want to have a to-do list table. This to-do list table is going to take in all of our to-dos. Just like that. And then we can render out a function called to-do list table. And this to-do list table function takes in our to-dos. And we can go ahead and we can type out our to-dos. So I'm just going to paste down that type for this because it's relatively straightforward. You can see here we have an ID, a name, an is
complete, and a created at. Those are the things we expect to get from this. And if we just return null for now, you notice we're getting a little bit of an error. That's just because of some stuff we have inside of our syntax. It's expecting us to use this array generic instead. There we go. That's working just fine. So now we have our to-do list table. And inside here, I want to have two sections. First, if my to-dos.length length is equal to zero. I want to return an empty state which luckily
tanstack start or not tan stack shad cn has an empty component for us which is really great. So we'll just install and add that empty component real quick. So inside of here I want to do a quick return. I want to use that empty element. Inside this empty element I'm going to have an empty header. Let me make sure I get that properly added. There we go. An empty header. And inside of here I'm going to have my list to-do icon which is just a random icon. And the variant for this is going to be
icon. There we go. Also, I'm going to add a border and border dashed onto this. There we go. Also, I need to make sure that this has empty media. There we go. That's going to be where my icon is put. And that's where my variant of icon is added on. Now, you notice nothing's showing up on our page right now. And that's because we have a to-do in our application. So, I'm just going to npm run db studio to delete that to-do that we manually added just so we can
actually see what our application should look like in the empty state of everything. So here, I'm just going to click on this and we'll delete that one to-do. And if we refresh our application, you can now see we get that empty state with that icon. Obviously, we should add in an empty title, though. So, we'll come in here with an empty title that just says no to-dos. There we go. And we'll come in with an empty description. I'll copy down the text for what that's supposed to say here.
And we'll make sure that we import that. There we go. So, it just says try adding a new to-do. So, we get our text right there. And then we can have our empty content. And inside of here, this is just going to be a button that essentially links back to the page that we had up here. So, I'm just going to copy this entire button. Bring it down and then slightly modify it. For example, we can get rid of the size on here because we want it to be a normal size button. And then you can see it
says add to-do. And when I click on it, it brings me to that new to-do page. Now, this new to-do page is going to be relatively straightforward because most of the logic for it is going to be inside of a form component. I'm going to paste down the actual component itself. And we're going to make sure that inside of Shad Cen, we add the card component. And I'll go through what's going on inside of here because really most of this has nothing to actually do with tan
sag start but it's really just some shad code. So you can see if we actually go to that add to-do page I give this a quick save. We are getting an error because right now this to-do form doesn't exist. So let me just comment out the to-do form section. But now if we just give that a quick refresh you can see this is what our page looks like. Essentially we have a button at the very top that just is a link that goes back to the homepage with a left arrow. You can see when I hover that and
click we go back to the homepage. Then we have a card that just says add new to-do. create a new task to add to your to-do list. And then below that is where we're going to have our to-do form, which is a brand new component we're going to create. So, let's just comment that back in and create that component. So, inside of our components folder, let's create a to-do form.tsx file. Export function to-do form. And we'll just return null. So, at least all the errors on our page should go away. This
has just to do with some sorting and stuff. So, let me just fix that. And again, this is just sorting. They should be put up. But you can see everything else inside of here is working. And if we go inside of here and we return something else, like for example an H1 that just says hi and we give this a refresh, you can see there's that text hi right there. Now inside of this form, we could use like react hook form or tan stack form to do our form library. But
since it's such a simple project and I really want to focus on the tanstack start portion, we're just going to do this form entirely from scratch. First of all, we're going to need an input component from shaden. And I'm also going to be getting my own built-in loading swap component. You can install this by just doing at WDS/loading swap. And you can download that component. And I have a bunch of other components on this registry as well. I'll link it in the description for you
if you want to check out all those different components. But now that we have those installed, we can go ahead and work on creating our form. So let's come in here with a form. And inside this form, we need to have our input element. Let's import that properly. This is going to be autofocus. Autofocus. There we go. So it's automatically going to be focused. We'll close out of that input. And then we can set up everything we need. For example, we're going to have a ref on here, which
is going to handle all of our state for us. So you can come in here and create a ref. const name ref equals use ref. And you'll notice something interesting. If you're used to something like Nex.js, you would need to go to the top of this file and add in a use client since we're using client-based code. But inside a tan stack start, there's no such thing as use client or use server. Everything runs on both the client and the server unless you specifically tell it to only
run on the server or to only run on the client. And this is a little bit of a double-edged sword. It's kind of annoying because now you need to manage all of that. But it's also really really nice because when I want something to run on the client, I just tell it to only run on the client. And if I want it to run on the server, I tell it to only run on the server. We've already seen this a little bit when we were actually back on this page here at the very top when we created a server function.
That's essentially telling us that we should do different code on the client versus the server. The server should run this directly. The client should make a fetch request. But we also have two other functions we can look at. We have create server only function. This is a function that only runs on the server. And if you run it on the client, it'll throw an error. Essentially saying, hey, you can't do this on this client. This is server only, so it'll throw an error.
We also have create client function. This is the exact same thing. If you run this on the server, it'll throw an error, but if you run it on the client, it'll work perfectly fine. And then we have create iso morphic function. And this one's a little bit interesting because it allows you to do a different thing on the server versus the client. It's the same thing how create server function does different things on the server and client, but this allows you to create your own. For example, inside
of here, all we need to do is just says server and pass it a function. And this runs on the server. So I can say console.log onserver. And now if I call this function on the client, it's going to run the code inside here. For example, console.log on client. There we go. So now it's running different code whether I'm on the client or I'm on the server. And I can just say const function is equal to that. And now I can call this function. And no matter where I call it, it's going to call a
different section. So, this is great when you need to do different things on the client versus on the server. For the most part, though, create server function is the thing you're going to use 90% of the time. And then client only and server only are useful for making sure you lock certain things down and throw errors if they're used in the wrong location, just so you don't accidentally expose things that you're not supposed to. So, now back to where we were. Let's come in here. This is an HTML
input element. Just like that. There we go. So, now we have our ref set up. And the rest of our props are relatively self-explanatory. We just have a placeholder here, a class name that makes it fill the full width, and then finally an area label just so it's labeled inside of our actual element. Then we can come down with our button. This is going to be a submit button. And we're going to disable this if we are loading. So we'll just create our own variable up here. Const is loading. Set
is loading. Make sure I get that all capitalized properly. And that'll be a state variable. And we'll set that to false by default. There we go. So now let's get inside of our button. We're going to use that loading swap component that I installed from my own library. This takes in an is loading prop. That's the only thing we need to pass to it. And we can just say is loading. And it also takes in some class names. So for example, we can say flex gap 2 item center. And that allows
us to take the content we put inside of here, which is a plus icon. There we go. Followed by the text add. Just like that. And now you can see we get that button showing up right there. Now, we do need to add some classes onto our form. So, we'll say flex with a gap of two just to space those elements out. And now, if we just zoom this out a little bit, you can see I can type some text in here and click add. And it's going to refresh my page because it's doing the default behavior of a form.
So, let's come in here and add our own handle submit function that's going to handle this for us. Make this a little bit easier to see by making it wider. Function handle submit. That's going to take in a form event. And we specifically want to prevent the default behavior of that. And then we want to actually do some type of server action. If you're used to nextjs, this is where you would call a server action. In our case, we're using a server function for this instead. So I can just
say add to-do equals create server function. And this server function is going to be a method that is post because we're going to be posting this data to create a brand new to-do. We're also going to be passing in some input. So we're going to be using the input validator inside this section as well. Now the input validator just takes in some type of data and then you can do your different validations and then return whatever data you want. So you can see here the type is unknown and you
can validate this yourself or in our case we're going to be using zod for this. So we can just say z.object just like that. And I believe we need to install zod. So let's come in here and we can say npm i zod to install zod as part of our library. And then inside of this we can just say we have a name which is a Z string with a minimum value of one. And let's just make sure we install ZOD. Looks like it's not quite working. So I can just say import Z from Zod. There we go.
Now that's been imported. And now we have an add to-do function. We have some input validation that's automatically handled for us. And now when we call our handler section, this handler is going to be a function that takes in a bunch of information and then we can return whatever we want. But if we look inside of here, we have this data property. And this data property is equal to whatever our input validator is. So our data is an object with a name of string. If we
change this input validator, that'll change our data here directly. And the nice thing is is this input validator runs for us. And if it's invalid, it'll return that back down to us. So now we can add this information to our database. We can say DB do we want to do an insert on the to-dos table and we want to select some values. So we can say the values here is just going to be our data. We're just going to insert a new value with a particular name. And we also need to set the is complete to
false. Just like that. There we go. That gives us all of our different data that we need. And then assuming that was successful, we just want to redirect the user back to the homepage. So we can use the redirect function for that and tell them where that we want to redirect them. So we can say to and then we can pass it in where we wanted to redirect, which is just this slash page right here. And it looks like I didn't import this properly. There we go. Looks like it's not importing. So I'll
just manually import it. There we go. And now you can see that this is working. And again, I get that same autocomplete. But to make this redirect actually happen, we needed to throw this as an error. So if we just say throw redirect, that's going to interrupt everything that happens in our page and redirect us to this new location that we want to go to. So now I've created a server function, which is really an action. If you're used to Nex.js, this action has some type of ZOD
validation. You can use your own or any other type language you want. We're just using ZOD. And it's going to execute some code such as adding this code to my database. I should probably make sure I await this as well. Just like that. It's adding my code to the database, redirecting me back to the homepage. And now all we need to do is run this code. And like I said, if we run this on the client or the server, it doesn't matter because it's going to behave the same.
It's just going to either make a fetch request for us or call the function as is. So in our case, in this handle submit, we can just call add to-do and we can pass it in our name. So first, I should probably just say if the name current or sorry, name ref.curren current dot value is equal to an empty string then we just want to return because we don't actually want to execute this. There we go. So I can just say coname is equal to this and then we can say name just like that.
So if the name there we go if the name essentially doesn't exist if it's equal to null or it's essentially an empty string we don't want to run our code otherwise we're just going to pass up our name just like that. Now you are seeing that we're getting an error and that's because we actually need to pass this up as data and then we can pass along those properties. So it always goes inside that data object. And then we can do whatever we want inside of here with this section. In our case,
I'll just throw a quick await on here. Make sure that this is an async function. And then before we do the code, I want to set is loading to true. And then I'm going to copy that down and set is loading to false afterwards. So this just handles my loading state in case we end up having an error or something along those lines. So now I have my entire application set up and we should see we should be able to actually create to-dos. So let's come over here. I'm just going to say new to-do and I'm
going to click the add button. You can see we get our loading state showing up and it looks like it's just spinning forever which means we probably have an issue inside of our code. Let's inspect to see if we have anything showing up in our console uncaught in promise. We have some random stuff going on. It looks like it is trying to redirect us to that location. You can see we get a 307 status code but it looks like it's not working properly and that's because I
didn't actually handle my redirects correctly. When you have a server function that you want to be able to handle redirects and other things from and you want to call it inside of your client side code, you need to wrap it inside of a use server function hook. You just pass it the name of your server function. In our case, add to-do. And we can just say const add add to-do function is equal to that. And now this will actually handle all the redirects, error handling, and everything else
inside of our code. So we can say add to-do function. Call that instead of calling the server function directly. And now if we just try to create something, it should just work. So we can say second to-do. And I can click add. And you can see immediately I'm redirected to this page. And we have zero of two completed to-dos because the first one created successfully. It just didn't redirect me. and the second one was created successfully and redirect me back to this page. So now let's actually
render out what that table of data is supposed to look like. So back into this index page, scroll all the way down to our to-do list table. Here's our empty state we have handled. Now I want to return a table instead. Now the first thing in my table is going to be a table header. And this table header is going to have a row and some head items inside of it. So we can come in here with a table head just like that. Now this first item inside of here is an empty
object cuz this is where my checkbox is going to go. Then we have the name of our task. We have the created on date. And we also have essentially our actions which have a width of zero just so they take up the least amount of space possible. This is like my delete and my edit button. Then we're going to have our table body. And inside the table body, we want to have a bunch of rows. So we're going to take our to-dos. We're going to map through each one of our
to-dos. And for each one of our to-dos, I want to render out a to-do table row, which is a custom component I'm going to create. So we'll say to-do ID. and we'll pass along all of our to-do information. There we go. So, let's say that we want to have a function called to-do table row. This to-do table row is going to take in our to-do information. So, it's going to be a title or a name, which is a string. And actually, I can just copy this directly from up here
because it's exactly the same as this. So, let's just copy that down. There we go. And now we have access to all those different properties directly inside of here. And if we just return for example null, we should see all of our code is working like we expect it to. Now coming into here, we first of all need a checkbox component. So let me just add that from shaden. We can say checkbox. Just like that. That's going to install this component. And then we're going to have our table row.
There we go. And inside of this table row, we're going to have a table cell. This very first table cell is for our checkbox. So we can use that brand new checkbox component we just added. And we can also add in what the checked property is going to be. So, for example, is complete is that checked property. Now, if I give that a save, you can see we have our checkboxes showing up over here. They're both unchecked for now. Now, let's copy that table cell down cuz essentially we want
to do the exact same thing, but this is going to be for our name, just like that. So, that's going to render out our task name. And if we want, we can add some certain styles to that. So, we can come in here and we can say class name. And we're going to use the fancy CN helper for this. And we can say that by default, we want the font to be medium. And then if the task is complete, we want this to have a text muted foreground. And we want to have it a text line through, which I believe is
just line through. There we go. Otherwise, we don't need anything else. So, I might as well just make this a double amperand. There we go. So, now if this is checked for some reason, this is going to have a line through and be a slightly less dark color of text. Now, we can copy this down for our next table cell, which is going to be our created at. So we can just create a format date function which takes in our created at date. Function format date just like that takes in a date
and we can just say const formatter is a new intl.datetime formatter undefined and then we can have a date style of short. There we go. That should hopefully do our formatter. So, we can just return formatter.format. Whoops. And we pass it in our date. There we go. You can see there's our date being formatted for us. And of course, our styles are a little bit off on here. This one should be a text of small and a text muted foreground. We don't need any of the other styles on here. So, we can
just get rid of all of that. And give that a quick save and a refresh. And now you can see there's our text showing up in that slightly transparent color. Lastly, we're going to have a table cell here with no class styles at all being applied to it. And this is going to be where we put our different buttons. So, we can come in here with a div of class name of flex items in the center, justify at the end, and a gap of one. And then inside of here, I essentially want to have two
buttons. So, we want to have our very first button, which is just going to be a link to our edit page. So, we're going to say a link to, and this is going to go to slash to-dos, and then we want to go to the to-do ID, and then we want to go to edit. And to be able to pass along different parameters like that, we have this params option. We can pass along the ID which is going to be our ID property. So we can just pass it along just like that. Now we are getting
errors because this essentially route does not exist yet. So let's go ahead and actually create that route real quick. We'll go into to-dos. We'll create a brand new folder with a dollar sign ID. So this is a big difference between Nex.js and Tanstack start. You use a dollar sign instead of the colon to represent a dynamic parameter. And then inside of here we can create a brand new folder. This one is going to be for our edit route. So we can say edit. And then we can create a file
called index.ts. There we go. index.tsx. And there's all the code for that. And now if we go back to this application, you can see that we no longer have any errors here. And the nice thing is is this is all type safe. If I forget to pass along my ID, I get an error telling me, hey, you need to pass along your ID. And if I type something over here wrong, for example, you can see again I get the exact same error. So this is a great way of having type safety on all my
different routes. Now, my button for this one, I want to have a variant here of ghost. And then inside of this, I also want it to be small. So, we can say size is icon small. And of course, since we're doing a link, it should be as child. Then my two link is fine. I just want to have it say edit link or edit icon. Just like that. There we go. So, now we have that edit icon. And you can see right here that that small little edit icon is showing up. And I'm essentially just going to
copy this down a second time. And this one is going to be the trash two icon. There we go. And this one's not actually going to be a link. It's just going to be a button. There we go. And now you can see we have our trash two icon. I want to give this a variant of destructive ghost, which is going to be a variant that I'm going to create myself. So, we're going to come into here underneath of where we have our normal ghost. We're going to create a destructive ghost,
which essentially copies this exact same thing. The only difference is we're just changing accent to destructive. There we go. And if we give that a quick save, refresh over here, and I change my default text to be destructive. So we say text destructive, just like that. And that should be on my destructive ghost. There we go. And let's make it maybe not quite so bright. So we'll say like 10% opacity or something. Or maybe like 80% opacity. And actually, I'm just going to
rework how this entire section works because I'm not quite a huge fan of it. So let's just change that up real quick. There we go. So, essentially all I've done is I've changed it so it's text destructive by default and when I hover it, the background becomes that small little bit text destructive and then the text gets much brighter. So, really that's all I'm doing. It's just a red outline for my delete button and then a white outline for my edit button. Now, our edit button is going to be very
similar to our actual ad page. So, I'm going to take all the code from our ad page, go into our edit page. I'm going to paste that all in there. Just make sure I change my route up here to be ID/edit because that's the route that we're currently at. And I believe I need an extra slash at the end. There we go. That fixes that for us. And now, essentially, all I want to do is change around what text I have inside my cards. So, let me just go ahead and copy the
text over from this other application. There we go. And you can see that I just changed it to edit to-do and update the details of your to-do item. And then we still have our back button. So now, if I go to this edit page, you can see it says edit to-do, update details. And the back button still works just like it did before. Now, in our application, we want to load what our default to-do is. So, let's come in here and we'll create a loader function. And this again must be
a server function. So we can say create server function just like that. This server function is a method and the method for this one is going to be get. Then we're going to have an input validator because we need some type of input on this. And this right here I'm going to show you how to do it without zod. So essentially we have our data which is a type of ID that is a string. And what we want to do is we just want to return our data as is. So we're just essentially hard coding what this
particular type is and doing no error validation on it because I'm only calling this from the server. So I'm not really too concerned about the data validation. Next, we can come in here with a handler. And this handler is going to take in that data property. And then we're going to call some code inside of here. And this data property, as we know, if I actually spell that properly, has that exact type of ID string. So now inside of here, I can get my to-do by just saying await. If I make
this an async function, DB dot and I want to get query for to-dos. I want to find the first where the equal clause of my to-dos id is equal to that data do ID that I pass in. And this is essentially just going to be coming from my parameters. So that gives me access to a to-do. If my to-do is equal to null, well, I want to throw a not found error. Again, anytime that you have redirecting or not found, you need to make sure you throw it as an error. Next.js does that for you automatically,
but tan stack start makes you be explicit about it, which is quite nice because now it's clear that I'm throwing an error in this use case. then we can just return our to-do just like that. So, if we don't have one, we throw a not found. Otherwise, we redirect to whatever that to-do is. Now, if we scroll down, we can get access to that information inside of our loader. So, our loader right here is going to have an asynchronous function. And this function right here is going to take in
all of our different params. And then we're going to call some code with that. So, this params right here is an ID with string because it matches up the type safety based on the stuff inside of our URL. So, in we have an ID in our URL, that's what our param type is automatically set for. And now I can just call that loader function and I can pass in the data which in my case is just my params because I type them as exactly the same piece of information. This doesn't even actually need to be
async because it's not doing any await directly inside of here. So now that makes that loader function work directly right here and inside of my component. I can just get my to-do by saying route use loader data. Make sure I spell that properly. There we go. And now I have access to that to-do. And let's just pass that down to our form. Just like that. So now I have all that information. And just to make sure it's working, I'm just going to come in here with my to-do.name and give
that a quick save. Give it a refresh. And you can see new to-do is showing up right there. So it does look like it is getting my information, which is great. Next, I just need to make it so my to-do form can take in a to-do, which is of course optional. So we can say to-do, and it's going to be optional. And we need to give it a type, which the name is going to be string. ID string is completed. Actually, we don't even need that. All I really care about is the name and the ID property. So, let's
first go ahead and inside of our input, let's set a default value. The default value is my to-do.name. So, there we go. You can see that automatically filled in with that information down here inside my loading swap component. If my to-do is equal to null, well, then I want to render what I was already rendering before, which is just this plus icon code. There we go. Otherwise, I essentially want to render out the text update. Just like that. So, it's going to say update if we're updating.
Otherwise, it'll say add with that plus icon. Also, to fix this type error, I'm just going to put a question mark here because to-do could be null. That fixes up that type error. We have our type right there. Now, what we need to do is make it so that we actually update instead of adding our to-do right here. So, we need to create essentially a brand new server function that's going to be for updating a to-do. So, we'll say update to-do. This is still going to
be a post. Our input is going to take in a name, but it's also going to take in an ID, which is Z.string.min of one. Just like that. So, we're essentially getting now two pieces of information, the ID and the name. And we have both of those inside this data property right here. And now I want to do an update. I want to update specifically my data and I want to update it where this particular value is. So, we can say set and I want to do a wear equals to-dos ID is equal to my
data id. So now I have an add function and an update function. We can make sure we wrap those in our use server function because again anytime that you call these functions from the client, you need to make sure that you wrap them to properly use them. So we'll call this function right there. And now here where we're doing our loading, we can just have a simple if check. If our to-do is equal to null, well then we want to add the to-do. Otherwise, we want to await update to-do function.
And our data for this one is going to have our name and our ID, which is just to-do ID. So, it's passing along all the information that it expects to have. And of course, make sure that we call this update to-do function, the one that we have inside of our use server function. So, now let's give that a save and see if it works. We'll just change this to new to-do 2 and click update. You can see it's saved, redirecting me back to this page, and it has updated directly
right here with new to-do 2. Let's change my second to-do and I'll just change it to say first. Update that. And now you can see it says first to right here. Now, so far, everything that we've been doing in this application has been calling from the client to the server, saving some information, and then being redirected back. It's been very serverheavy. But what happens if we want to do a lot of these updates more on the client instead of on the server? Let's
go ahead go back to our index page. And we have essentially a delete button that we want to be able to handle, as well as the ability to toggle this checkbox because right now we can't do that. So, I want to first focus on this delete button one because it's actually going to be the easier of the two for us to handle. I'm going to be using a component here. This is an at WDS component. So, it's one of my own built-in components. And it's called action button. And this essentially
allows us to create a button that handles a loading state and a bunch of other information for us automatically. All we need to do is pass it along in action. I originally created it for next.js, but it works perfectly fine with tanstack start and any other server action adjacent library that you want to use. So, you can come in here with this action button just like that. And let's try to make sure we import that. Looks like it's still trying to import. There
we go. Now that that is done, we can import our action button component. And essentially, we put the exact same stuff and use the exact same properties that we would on a normal button. So it works just like a normal button. The only difference is we can pass in an action. And an action just needs to be some function that returns to us a promise that evaluates to an object with an error of true or false. That's all we need to do. So we need to create a function, which in our case is going to
be a server function because, as you can see, we're calling this on the client, but we want to be able to run code on the server. Anytime you're doing code on the client that you want to run on the server, it must be a server function. So we'll call this our delete function, which is just going to be create server function. This create server function is going to be a method of post. You'll notice that there's no delete or put or patch method. Post is for any type of update,
whether it's delete, update, create, doesn't matter. And get is for anytime you get data but don't modify anything. Next, we need to have our input validator. This one is going to be relatively straightforward. It's just going to be a Z dot object. And that Z object is going to be an ID of Z.string with a minimum value of one. There we go. And looks like my import for object didn't quite work properly. And that's cuz I tried to wrap it in an object. Don't know what I was
doing there. There we go. So that is done. Now we can create our handler. And we know our handler is going to have our data property on it. In our case, the data is just our ID. So, all we want to do inside of here is a simple DB dot and we want to do a delete from the to-dos table. Whoops. To-dos. There we go. And we want to delete it where the equality of our data do ID is equal to our to-dos ID. There we go. So, now we're just deleting that particular element from
our database. And like I said, for this to work as an action in our action button, it just needs to return to us a promise. And since this is asynchronous, which we'll make it async in a second, it is automatically going to return a promise for us. We wanted to return a promise that has an error value of true or false. In our case, there's no error. So, we just returned error false. Now, this will work perfectly with this action property here. And all this action property does, if we look inside
the action button and we scroll a little ways down, you can see here on our button, all it's saying is on click perform action. And if we go to perform action, all it does is wrap it in a transition, call it the action, and then handle our loading state. And if we have an error, it renders out an error message for us automatically. So it just handles loading error states, all that different stuff for us. We don't have to do it manually. But now we can use that delete function in here. And since
again, we're on a client trying to call code on a server, we need to wrap it in that use server function hook where we pass in our delete function. And we can just say const delete function server or whatever. It doesn't really matter what the name of this is. We can pass that in as our action right here. And this action specifically needs to pass along the ID. So, we can say to-do, I'm sorry, ID is to-do ID. Just like that. I believe actually just ID. There we go.
And of course, this must be passed along as a data property. I always forget that when I'm working with tan stack start. And now that's everything. You can see our action button is now working. We no longer have errors. And if we refresh, you can see our code over here is fine. And if I click delete, we should hopefully see it actually deletes this element. I click delete. You'll notice it gave me a small loading state. And then instantly it went back. But you'll
notice it didn't actually delete the element. But actually, it did. When I refresh my page, you'll notice it's gone. But it didn't automatically refresh for us. We need to actually handle that invalidation of the route for us. So inside of our application, we can come in here and with our router, which we can just get from the router hook, use router. There we go. That gives us access to the router. Inside of here, we can say router.invalidate. And that's going to invalidate everything in
our router. So let's just say here const data is equal to that. We'll call it response. Make sure we return that response. So that way our action is happy with that information. Make that async just like that. And now this router invalidate is going to be automatically refreshing our page for us. So now let's make sure we just do a refresh. Everything's fine. When I click delete, you can see it deleted that. Refreshed my page. Notice there's no to-dos and brought me back to the blank
slate. Let's just create a random to-do. Doesn't matter what it is. You can see it's here. Delete again. It's gone. Now let's try to add two to-dos. And then if we delete one of them, you can see it refreshed. Everything 0 out of one completed. Everything was perfectly updated in our application. So now let's go ahead and do the exact same thing for the checkbox. Essentially anytime I click on the entire row, I want to make it so that my checkbox is going to execute. So I can just throw in an on
click on the entire row here. I can get my target which is E.target. Make sure I get the E value from here. There we go. And this is an HTML element. So we can just cast this as an HTML element. So we have all the properties we need. And first of all, I want to say if the target closest is equal to our data actions. There we go. Then I just want to return just like that. And I'm just going to wrap this action section in that type. So we say data actions. That just makes
it so that if we click, for example, the edit button, it doesn't toggle my checkbox. So anytime I click inside my actions, it essentially ignores the click on the entire row. Otherwise, if I'm clicking on the row itself, I want to run some type of toggle function. So let's create up here a brand new function for toggle. So we can say toggle function just like that. It's going to be a server function. This is going to take in an ID as well as an ismplete property. This is complete is
going to be a boolean just like that. So now I have those two properties being passed along. Let's see if I can get my spacing on these a little bit better. There we go. And now inside my handler I have that data ID and is complete. I want to do an update and I want to set some data which is just my data. And I want to do it specifically just the ismplete property. Data.ismplete where the to-dos ID is equal to that ID. And I don't even need to worry about this error false or anything like that
because I'm not doing this inside of an action button. So now I have my toggle function. I can do the exact same here. Copy this. Toggle function. Toggle function. There we go. Now I have the server version and I can just call toggle function and I can pass it along the different things that I want to toggle. So, inside of my data, my ID is my to-do, and my is complete is the opposite of my to-do.mplete. There we go. And actually, these aren't wrapped in to-do at all. They're just on
their own. So, I can just say like that. There we go. We also want to make sure that we await this, throw this in an async function, and invalidate our router afterwards so everything refreshes properly. So, we can say router.invalidate. And that's going to run that code for us. So, now if I click on this, you can see it checked my checkbox. And if Iclick it, unchecks my checkbox. Now, there is a small problem. If we scroll down to where we have that toggle function, I'm going to throw in a
wait here. Await, new promise, resolve, set timeout, call resolve after 1 second. And what's going to happen is you'll notice a small delay. I'm going to click right now. And you'll notice it takes a whole second before it actually shows this information in my UI. This is obviously not ideal. So, we want to use some type of optimistic loading. A really easy way for us to do that is to just create a piece of state is complete. We'll just call this is current complete. There we go. Set is current
complete. And we'll set that to a use state that is by default set to whatever our current complete property is. Then we can use this everywhere else. So here our checked is equal to is current complete. And all the way down here where we have set is current complete. We can just throw that in just like that. We can say set is current complete. And we can set that to whatever here the opposite of our current complete is. There we go. And here I want to use my is current
complete. I'm also going to wrap this inside of a transition. So we're just going to say start transition just to make sure that it delays the updates of our state for these ones while it updates the other ones immediately. And we'll make this async. And I no longer need this to be async. There we go. So now it's just going to update all this code inside of a transition. And hopefully should be much more instantaneous. We'll do a refresh. And when I click, you can see it
automatically unchecks this section while the rest of my route, for example, up here take 1 second to refresh because we're not handling those in an optimistic update. So now you can see we're getting our optimistic update on our checkbox and everything else is delayed by a second. Now, we could make this UI much better by fixing all those other pieces as well, but in our particular case, I don't really want to. One thing I do want to change though is my is current complete should be set
right here. That way, the actual inline line through this is updated as well. Just this section isn't, which is perfectly okay. This is just a demonstration of how you could implement this type of feature. So, we've been slowly moving a little bit further away from the server and doing more things on the client. But what happens when you want to render something just on the client or something that's very client focused? We're going to go all the way up to the top of our application, all
the way up here where we have our header section. And inside this header section, I want to add another button. And this button, I'm going to call this my local count button. And this local account button is just going to be a button that stores a count inside of local storage. So, let's go ahead and create that inside of my components. local count button.tsx export a function with that name. And for now, we'll just return null. And if we go back in here, we should be able
to import that function. And there we go. We now have essentially nothing rendering because we don't have anything in here. Now, inside of here, I essentially want a button. And this button is going to be relatively straightforward. I just want it to render out a count inside of it. There we go. Close off my button component. And for this button, I want the variant of it to be outline and the size to be small. There we go. So now, if we just make sure we have a count variable.
Oops. Make sure it's set to use state. And we'll set that to zero by default. You can now see we have this nice little variable right here for our button. We can add a little bit of spacing between these. For example, we can just say flex and gap of two or something like that just to space them out. And now we have a button. Clicking it does nothing yet, but that's perfectly okay. I'm going to come in here with an on click and I'm just going to change my count. So we can say
set count to our count + one. There we go. So now every time I click this button, increments my count by one. But when I refresh, it obviously deletes that information. I want to store this in local storage though. So we can just come in here with a simple use effect. And I can say that I want to take my local storage and I want to set a value. We'll just call this count. Just like that. And I'll set it equal to whatever my count is. And I want to do this every
single time that my count changes. There we go. So now if I were to change my count, it should be updated in my local storage. But of course, it's still not quite working like we expect. This is because first of all, this must be a string. So we'll say count.2 string. And we also need to load the count. So we'll come in here with a use effect. And this is going to load our count information. So we can come in here set count is going to be calling just some load count
function. I'll just create down here load count. Make sure I spell function properly. And inside here, I'll just say stored count is my local storage.get count. And then we need to convert it to a number. So if my stored count exists, I'm going to parse this as an integer. Otherwise, I'm going to return zero if it doesn't exist. So now we're loading up our count. So now you can see I'll change this value. Refresh my page. And you can see it saves that value. You'll
notice a small flicker though where it goes zero then seven. That's because it starts at zero and then my use effect runs and it changes it to seven. Often times you would want to have your load count directly in here and you'll have no need for this use effect at all. This is the ideal way to do something like this in a purely React-based application. But if we do this, we're going to get some errors. You notice it doesn't look like we get errors, but if we actually inspect our page, go to the
console, we're going to get this massive error. And really the big thing about this error is essentially here switch to client rendering because a server rendered an error. Local storage is not defined. I am trying to call local storage on the server because remember all of our code runs on the server and on the client and this is going to be a problem. Now to make this clear since I'm using local storage inside of here I would recommend making this a client function. So we could just say const
equals create clienton function. Just like that. There we go. So essentially now it's very clear that this is a client only function. And again when I try to render my page and I inspect and I go to my console I'm going to get essentially the exact same error. is going to be slightly different, but it's for the same reason. Switch to client rendering. Client only functions can only be called on the client. So, what exactly is the best way for us to fix this? Now,
obviously, you could just throw zero in here and use a use effect to load your information and that's going to work perfectly fine. And in many scenarios, that is actually what you would want to do. So, essentially just changing this back to zero and throwing that use effect back in load count. This is going to solve the problem for us again. It's going to start at the wrong particular variable. So, if we increment this up, refresh, and actually for my load count, I need
to make sure I set my count. There we go. That was my problem. So, now if we just give this quick refresh, increment it, refresh. You can see it starts out at zero and instantly increments itself up to five. That's fine for most scenarios that we're going to be working with. But sometimes you have code that it runs on the client and there's really no point in rendering it on the server at all because it's just purely client focused, like maybe a really complicated
canvas thing or like a lot of drag and drop stuff. So, what we can do is we can actually wrap our code in a client component instead. So generally I would do it inside this exact same section. So I'm just going to create a brand new function called client section. And inside here I'm just going to take all the code and I'm going to paste it down in the client section. And up here I'm going to return that client section. So essentially all I've done is just moved
my code into this function right here. But I can now wrap this in this thing called client only. And any code inside a client only is only rendered on the client. It never even renders on the server. And you'll notice if we look over on the right hand side, there's going to be a brief section where this button is not even on the page at all. It's going to flash in existence because it starts out not on the page. And when I refresh, it actually shows up on the page. It's a little hard to actually
demonstrate that cuz it's so quick. But essentially, the button is not on the page for the very first render because the server doesn't render it. And then the client renders it and it shows the information right here. This is a great way to get around issues where you essentially have a component that is very client heavy and has nothing to do with the server at all. you can just say that you only want to run it on the client and then it's going to not have to worry about, you know, things like
local storage not being defined and so on. And when we do this, we can actually just come in here and we can load our account directly in here. Get rid of this use effect cuz now this code only runs on the client. And if I just change this to like six, for example, refresh, you can see when that button first shows up on the page, it has that value of six. It just starts not on the page. And you can pass a fallback here if you want. For example, I just pass a bunch
of text. And now when I refresh, you can see that text briefly appears on the page cuz that's my fallback value that renders while waiting for the rest of this to render. So the fallback renders on the server and then this renders on the client. Now all the code for this entire project is going to be linked down in the description below if you want to take a look or look at it. But that's essentially the complete starting guide to Tanstack Start. If you want to
see some larger, more in-depth projects on Tanstack Start, let me know down in the description below. I actually really enjoyed working with this framework and I'd love to create more tutorials like that for you. With that said, thank you very much for watching and have a good
Tanstack Start is a new meta framework that aims to solve many of the annoying problems with Next.js. In this video I will be showing you how to create a complete fullstack project using Tanstack Start and explaining how it compares to Next.js. By the end of this video you will know everything you need to know about Tanstack Start to build your own fullstack projects. š Materials/References: GitHub Code: https://github.com/WebDevSimplified/tanstack-start-todo-list WDS Shadcn Registry: https://wds-shadcn-registry.netlify.app/ š Find Me Here: My Blog: https://blog.webdevsimplified.com My Courses: https://courses.webdevsimplified.com Patreon: https://www.patreon.com/WebDevSimplified Twitter: https://twitter.com/DevSimplified Discord: https://discord.gg/7StTjnR GitHub: https://github.com/WebDevSimplified CodePen: https://codepen.io/WebDevSimplified ā±ļø Timestamps: 00:00:00 - Introduction 00:00:59 - Project Setup 00:06:26 - Database Setup 00:10:33 - Your First Route 00:13:24 - Server Only Code 00:16:52 - Home Page 00:24:10 - New Todo Form 00:35:03 - Todos Table Component 00:41:45 - Edit Todo Form 00:47:30 - Client-side Updates 00:55:10 - Optimistic Updates 00:56:40 - Client-only Code #TanstackStart #WDS #NextJS