Build a Project Management App with Remixwith Chance Strickland
Remix is a new tool for building and deploying fullstack React applications with a keen appreciation for the web's foundations, built by the team behind React Training — and it was just made open source! Come join us on Some Antics as Chance Strickland shows us how we can build a project management app using Remix.
More From Chance
- Follow Chance on Twitter
- Chance's Egghead course on React and TypeScript
- Chance's stream on building accessible tabs in React
- Chance's stream on building accessible dialogs in React
[00:00:12] Ben: Howdy, howdy, y'all. Welcome back to Some Antics. It is another Tuesday, another day talking about building great user experiences for the web. Today, I am joined by Chance Strickland. Chance, hello! Welcome back to the stream!
[00:00:27] Chance: Ben! Always great to be here.
[00:00:30] Ben: Always great to have you on!
[00:00:31] Chance: Really looking forward to it.
[00:00:33] Ben: Yeah, absolutely! And before we dive in, I want to thank Michael Chan for resubscribing, and I want to thank Jason for the raid! Thank you so much. That is awesome. Welcome, everyone. If you're unfamiliar, Some Antics is a weekly Twitch stream where I bring on guests from around the webdev industry and we talk about, you know, building great user experiences with the web, with a focus on accessibility and core web technologies. So if you're coming from Learn With Jason, the format should probably be fairly familiar, just with a focus on, like, that accessibility and core web tech.
[00:01:09] So, yeah! Before we dive into what we're doing today: in case folks haven't seen you around, Chance, would you like to just kind of introduce yourself a bit?
[00:01:21] Chance: Sure, yeah! So my name is chancethedev on Twitter, or chancethedev anywhere. I guess my legal name is Chance Strickland, but nobody ever remembers that, so I like chancethedev. It's nice and snappy.
[00:01:33] But I work for Remix. If you don't know about Remix, Remix is a React framework. We just open-sourced and released to the public a few months ago. And we're at version 1.1.1 right now — make a wish. So, we're really excited about Remix. Remix is also really focused on web foundations, web fundamentals, so I think it makes a great pairing for this podcast. And I'm really excited to dive into some of Remix's APIs and how we think we enable you to make better websites. So, really excited about this.
[00:02:05] Ben: Awesome! Let me go ahead and start sharing my screen! And if you aren't already following Chance on Twitter, you absolutely should — @chancethedev. And then we're going to be talking about Remix, and you can find out more about Remix at remix.run!
[00:02:26] Excellent, yeah! Cool! So let's go ahead and, I guess, just kind of dive in. So, you sent me a repo ahead of time and I had to do some setup-type things. This repo will be available later, but my understanding is it's not QUITE ready to start, like, just dropping links places.
[00:02:49] Chance: Yeah, so just for everyone's reference — I've already apologized a million times to Ben — but my life is, I would describe, nothing short of chaos at the moment. I've got just, like, lots of externalities that are just… let's just say I have not made as much progress on this code as I'd like to. But that's okay. We're going to do it live, and we're going to still explore all the important things. But eventually — and by "eventually," I mean hopefully this week — we'll get everything cleaned up and published so that we have a solid foundation for sharing with everyone.
[00:03:27] Ben: Alex, thank you for the sub! Super appreciate it.
[00:03:30] Yeah, so I had to do a few setup things. These are environment variable things, because we're going to be doing some database stuff. But besides that, I have not really had the chance to do anything with this codebase. But just, I guess, this caveat of, we are working with some scaffolding that's already been built out, mainly so that things like database connections and styles — stuff, that's not really today's focus — just so that that stuff could be squared away ahead of time. So, yeah!
[00:04:01] Chance: Yep. So, and to build on that, too… So what I wanted to do for this stream — I know a lot of streams, you'll build these small little things, which makes sense because you've got, what, an hour, hour and a half, to get through something, and it's like, you can't build a production app in an hour and a half. You're just not going to do that. So, like, what I've got here, what I'm hoping to have here is a really solid starter to what would ultimately be a production-ready app! And we're not going to build the whole thing in an hour, hour and a half. What we're going to do instead is we're going to take a project that's already set up and we're going to just walk through some of the code and explain what's happening and try and explain some of Remix fundamentals that way. And then, when we get to the end of that process, what we'll do is we'll add a couple of new features that will also help us understand some of Remix's core features.
[00:04:47] Ben: Alright, cool! So, where should we start? Should we be diving into the code, or should we pull up this project? Like, should we be talking about what Remix even IS in the first place?
[00:04:58] Chance: Yeah, so Remix at its core is a server-side rendered React framework. If you are familiar with frameworks like Next.js, it's similar in a sense, but also not really. I would say it's more comparable to full-featured web frameworks. Like, if you're a Ruby dev, you might come from Rails. If you're a PHP dev like myself, you might come from Laravel. These full-featured server web frameworks, these full-stack web frameworks to really help you bootstrap a server-side rendered application really quickly and efficiently and provide a solid user experience. So that's, at its core, what Remix is trying to be and what I think we are.
[00:05:57] Ben: Yep!
[00:05:59] Chance: And we've got a couple dependencies that are going to help our dev process a little bit. We're going to run
npm run dev as well, which is going to start up our web server using PM2. PM2, for those who don't know, is a process manager. It allows you to run multiple processes concurrently with some added configuration options. If you're used to running the
concurrently npm package, it's sort of similar but a little bit more full-featured. So we're going to be running a few processes simultaneously.
[00:06:29] Unlike some other tools that you might be used to in the React ecosystem, Remix sort of pulls back a little bit on some of the build tooling abstractions. So we don't bundle your styles automatically. We don't abstract any of that stuff. We expose that directly to the user so you can handle compiling all your styles with whatever tooling you want. You wanna use Sass? You can run Sass. You wanna use PostCSS? You can run PostCSS. You just wanna load some plain ol' CSS stylesheets? You can totally do that, too. What we're doing here is we're running a process that's going to compile a lot of our styles using PostCSS so that we don't have to worry so much about modern syntax and that kind of thing. But other than that, it's a pretty light process here.
[00:07:11] And we're also going to run our server as well on a separate process. So we're running an Express server on one process, the Remix dev server in another, and our CSS processing on another. And that's what we're doing now. So if you want to open up localhost, port 3000, we will… In a browser, sorry.
[00:07:29] Ben: Yep!
[00:07:29] Chance: Yeah, we will see our application!
[00:07:34] Ben: Alright!
[00:07:35] And thank you to everyone who's been following. jFeliWeb, Drumpy, thank you for following. Ben, thank you for the sub.
[00:07:42] And I see a few people talking about our sweaters. I actually do want to highlight our sweater. I have to, like, kind of stand up for this. I am wearing a T-rex sweater. The T-rex is wearing the same sweater. If you look carefully, you can see that the T-rex's little claws are poking through the sleeves. So, just so you know, Christmas isn't just the brand today. We're still going all in on T-rex Christmas. So, just had to do that.
[00:08:10] Chance: Yeah, and I'm actually feeling a little left out because I scrambled to find this hat, but I have a sweater I really love, and it just can't find it. But it's a green Christmas sweater with the leg lamp from "A Christmas Story" on it, and it's, like, my favorite thing ever. And tragically, you'll just have to imagine it in your head and pretend that I look way cooler than I do. But right now, Ben has got me on the Christmas game.
[00:08:32] Ben: Bubbz, thank you for the sub. Yeah, so — and also T-rexmas, I think that is the official name for this event!
[00:08:39] Chance: [chuckles] I love that.
[00:08:40] Ben: We are celebrating T-rexmas.
[00:08:43] Yeah. Cool! So I've got this little sign-in form. I don't currently have an account.
[00:08:50] Chance: Yeah! So before we talk about the sign-in form, let's just talk about what just happened, right? Like you went to localhost:3000, and then it sent you to this sign-in form. How did we do that? So what's actually happening? So if we go back into VS Code for a moment. Let's just walk around a bit.
[00:09:08] Ben: Okay!
[00:09:08] Chance: So the first thing that we want to note here is that Remix works by various conventions, and one of the conventions that we use is we have this
app/ directory. And if you open up the
app/ directory, just like its name suggests, this is sort of where all of your app code lives.
[00:09:25] So right off the bat, when we run… when we open this URL in the browser, what's actually happening? So we have this entry server file and this entry client file. And as they might suggest, these are sort of the entry points for both our client and our server code. The server, as you might guess, is our server! It's just a plain old Express server, and it handles all the routing and it handles everything that a server would normally handle. And there is a, like, Remix wrapper for handling all of the requests to our server. And it's pretty straightforward, but the client code is where we hydrate our app. So if you're used to working in a React app, if you're rendering it on the server, you're going to render it once on the server with some ReactDOMServer methods, and then you're going to hydrate it in the client, which essentially just means ReactDOM is going to go through and map the server code to your client code and then start running all of the effects and doing all the things that we love for really fancy client-side transitions and everything, right? And so, think of those as our entry points.
[00:10:32] Our root.tsx is sort of the root route [/ɹuːt ɹaʊt/] — or root route [/ɹuːt ɹuːt/], if you're from one of those weird countries that pronounces those two words the same — our root route is… it's our root! What it suggests, right? This is what we load when we first open up our website, and it's responsible for rendering everything. So let's explore that.
[00:10:54] We've got this Document component, and we've got a few things that you might notice. First of all, it's just an HTML doc. That's all it is. There's no abstraction of that. It's just
<body>, and that's it, right?
[00:11:07] Now, we've got some… we've got a little bit of abstractions here, right? We've got this
<Meta /> component. We've got this
<Links /> component. These components are responsible for rendering the individual routes'
<Meta /> and
<Links /> functions, and we'll talk about all of those things later on as well, but those are ultimately going to generate the meta tags and the links tags for each of our individual routes.
[00:11:30] Now, inside of our
<body>, we've got our children. And we're using this as our layout, and we'll see where that is rendered in a few minutes. But we've got this script that is essentially just injecting some client-safe environment variables from our server. You might note that you can't call
process.env in the client, which is a good thing, because we've got some sensitive information in there usually, like secrets and whatnot. So we do want some things on the client, perhaps, so we assign this
ENV global variable, too, in the client. So, we've got this
<ScrollRestoration /> component that comes from Remix and handles scroll restoration between navigation. So if you are normally working in a client-side app, one of the headaches is that when you're dealing with client-side routing, a lot of times you lose your scroll position when you change routes.
[00:12:14] Ben: Okay!
[00:12:14] Chance: And that's what
<ScrollRestoration /> does for us.
[00:12:16] We've also got this
[00:12:39] Ben: Yeah!
[00:12:40] Chance: Like, maybe you're not actually using any state. Maybe there are no effects in your app, right?
[00:12:44] Ben: Mhmm.
[00:12:44] Chance: You don't need React at all on the client. We can remove all of React from the client by simply deleting the
<Scripts /> component, and we will never, ever have React in our bundle. And that's pretty cool, I think! We can drastically reduce the size of our bundles in the client if we don't use any of React's client-side features! Now, we're going to in this application, so we're going to keep that, but I just think that's a nifty little thing we can do. And since everything is running in the server anyway, the routing is still gonna work even if that
<Scripts /> hasn't fully loaded yet and we haven't actually hydrated the client yet.
[00:13:20] So, now if we go down to our application, or our App component, we can see where we're calling a few things. We're calling this
useLoaderData. What the heck is that thing?
[00:13:29] Ben: Yeah!
useLoaderData is a hook that comes from Remix and it is what's responsible for getting data that is loaded on the server via the loader function, which we'll jump up to in a minute. Now it's getting this object back, and it has this
ENV variable. We're getting that from our server, passing it to the client, and passing it into our
Document which is going to assign that global variable, right?
[00:13:49] And then we're rendering this
<Outlet />, the child.
<Outlet /> is a component that… it comes from Remix, but it actually comes from React Router under the hood. It's a light abstraction on the React Router 6
<Outlet /> component. And this is responsible for rendering the route that we're currently looking at. Right?
[00:14:05] Ben: Mhmm!
[00:14:05] Chance: So think of the
<Outlet> is whatever route we're currently on, that's rendered by the
<Outlet />. So this handles all of our routes.
[00:14:11] Ben: Okay.
[00:14:11] Chance: And then we get this
<CatchBoundary /> thing. The heck is a catch boundary? So if our server throws an error, and we catch that error and handle it specifically based on the status code here, we can return a different error message based on whichever status that we got. So if we return a response that has one of these status codes, we can render a specialty error page for that status code.
[00:14:36] Ben: Okay!
[00:14:37] Chance: We've also got this
<ErrorBoundary /> component. You can think of the
<ErrorBoundary /> component as ANY error that is thrown on the server that we just forgot to handle! Right? Any "uh-oh" page or, like, "What the heck happened? We don't know. We forgot to handle that." So all of that stuff, if you throw any error on the server and we haven't handled it anywhere in our code, it's going to end up in the error boundary.
[00:14:58] Ben: So, just to make sure I'M understanding properly, everything that we're seeing in this root.tsx, this is the scaffolding for an entire application's interface. So, like, all of the pages ultimately sift through this logic. So we only have to write it once. We're not writing this for a bunch of different pages.
[00:15:17] Chance: Not quite, but almost. So…
[00:15:19] Ben: Okay.
[00:15:20] Chance: That is technically correct. You can get away with only having this and you would still have your app. Now, it's not gonna be super useful because you need routes in your application, right?
[00:15:28] Ben: Sure.
[00:15:29] Chance: But, so the conventions like the catch boundary and the error boundary? The way that these conventions work is that they're handled by the root, but if you DO export an error boundary or catch boundary in a specific route and you're on that route—
[00:15:41] Ben: Oh!!
[00:15:42] Chance: —it's going to basically bubble up until you hit the first boundary that you find.
[00:15:46] Ben: Okay!
[00:15:46] Chance: You can think of that as, like, the boundary for handling those errors. So if we have one at the route, we handle it there. But if we don't have one in a specific route, then it just bubbles up until it gets to the root, and it bubbles up through the route tree.
[00:15:58] Ben: Interesting.
[00:15:58] Chance: And we'll talk more about the route tree and nested routes and all that stuff here in a bit. But, yeah, essentially… the root is an example of what any route component technically could look like. We can have a loader. We can have
<Meta />. We can have
[00:16:12] So if you start at the top of the file, we can talk a little bit about some other conventions here. Now, I mentioned the
<Links />. We talked about rendering that
<Links /> component, that's rendering all of the
<link> tags from our route. If we export a function called
links, it returns an array of objects that is going to map to our
<link> tags and the
<link> tag attributes, right?
[00:16:35] Ben: Okay.
[00:16:35] Chance: So right now, we are loading the stylesheet. This is our global stylesheet and we are loading it in root because it's global, and we want it to load everywhere. Any links that we render in the root are going to render all throughout our application. We can also export links on individual routes and they are only loaded on those routes.
[00:16:53] Ben: Mm!
[00:16:53] Chance: Which is really great for styling because we don't have to worry as much about classname clashes that we may have in other styling mechanisms where you're loading potentially conflicting classnames, right? So one of the biggest reasons people reach for tools like CSS modules is to avoid these clashes, right? It's much less likely if you're loading stylesheet links in your individual routes, right? Because you know what you're rendering in that route. So you have a lot of control here based on exporting individual links depending on which route you you happen to be on. And those
<link> tags will be added and removed to the DOM as you navigate.
[00:17:35] Ben: Okay!
[00:17:36] Chance: The loader that we see below this is — and for those who are not familiar with TypeScript, I just realized that we might have some folks who are seeing some of these types and getting a little confused — the loader function, this syntax, the
export let loader: LoaderFunction thing, this is just giving us the types for the function that we're exporting. It's a definition for our loader type. And the loader function is an asynchronous function. It's only called on the server, and this is where we get that loader data from. When we call the useLoaderData hook in our route, we get it based on the return value from our loader. So we see in our loader function, we are returning this object with an
ENV key that has our site URL from the server.
[00:18:20] Ben: Okay!
[00:18:20] Chance: Now we're returning that data from the loader, and we call
useLoaderData in the route component. We have access to that data directly. That make sense?
[00:18:29] Ben: I think so. Yeah, there's a lot of pieces that kind of fit together, but I'm starting to see how… yeah, how this is fitting together. So, this loader, this is how we get information from the server to the client logic.
[00:18:43] Chance: That's exactly right. So anytime you need to get something from the server over to the client, and you only want to handle it on the server, that's what the loader's for. And this is actually a really powerful function because what we normally do when we're building React applications, if we need to go fetch something from the server, is we've got this component render sitting around in our application on the client and it doesn't have any data yet! We render it before we ever have any data, right?
[00:19:09] Ben: Mhmm.
[00:19:11] Chance: Well, how do you get that data? Well, normally you might use some sort of data wrapper like, you know, use React Query, right? Or what's the Vercel one? SWR, I think it is.
[00:19:22] Ben: I think so.
[00:19:23] Chance: Yeah, there's a bunch of different libraries that people use to abstract the Fetch API. But essentially, we just rely on the web standard. We let you fetch data directly on your server, return it, and by the time it's rendered in the client, we've already got it on the server. You don't have to do any of that stuff, right? There's no loading spinners. There's no "set some state after
useEffect" and, you know, waiting for stuff to load. It's just there, right? And if you have a fast server and you use caching, you can get that data really quick and have a really snappy app with fewer loader spinners, which is pretty cool. And we'll see some of that in action as we keep going forward.
[00:20:00] Ben: Okay!
[00:20:00] Chance: So yeah, now that I've explained away the root file, we can go back to our browser and see that we are sitting on a sign-in page. But we didn't go to the sign-in route, but the URL says we're on the sign-in route. We just went to the index. Let's go back to our code for a moment. Let's talk about how we got here!
[00:20:20] Ben: Yeah!
[00:20:23] Chance: So, if we look in our
routes/ directory inside of our
app/ directory — This is another convention in Remix. The
routes/ directory, as it suggests, is where all of our route files live. This is how Remix knows what to route to our URL from our URL. So from those requests, we end up rendering these specific components based on that
<Outlet /> we saw in the root, right? So our index route… So, let me back up. Whatever your default export from a route file is the component that you render when you see that route. Now, in our index route here, we aren't actually exporting a default component. We're not exporting a component at all. There's nothing here except this loader function.
[00:21:04] Ben: It's purely the server-side logic.
[00:21:07] Chance: Yeah! So you can think of… Routes that don't have a component, you can think of as request handlers. They're essentially just request handlers. So your loader is what is returned to you when you make a
GET request to this endpoint, to that URL. So if I request this URL in the browser, we're going to execute this loader logic on our server, and then the response that we get is what is handled by our browser.
[00:21:30] So, we see in the loader, what we're doing is we're calling this
getUser is just an abstraction that is essentially getting some session data, right? When we sign up for a new account or we log in, we're going to establish the session on the server. And then we're going to check the server to see if we have an active session. If we do, we're going to redirect you to that user's dashboard. If we don't, we're going to redirect you to the sign-in page. And that's how we get to the sign-in page in the browser.
[00:21:56] Ben: Okay!
[00:21:57] Chance: So now, since we are redirecting to sign-in, as you might imagine,
sign-in is adjacent to the
index.tsx file. We've got the
sign-in file. This is our sign-in route, and this is what we see in the browser!
[00:22:09] Ben: Okay.
[00:22:10] Chance: So we're importing a bunch of components and tools, but we're exporting a few things, too. We're exporting this
meta function. So our
meta function is similar to our
links in that it returns… in this case, it returns an object and the object has a key–value pair. You can return a title. You can also return meta descriptions, you can return OG tags — anything that would ordinarily render a
<meta> tag in HTML. It just follows the similar conventions. I'm pretty sure OG tags are the only type of
<meta> tags that don't use the title. And I think it's
content. I always forget.
[00:22:46] Ben: Yeah, I was actually just looking this up yesterday for some side projects I was working on. OG tags use the attribute
[00:22:58] Chance: Yeah, that's it.
[00:22:59] Ben: Whereas most other
<meta> tags expect it to be like
<meta name> for the name of the property. It's weirdly confusing.
[00:23:11] Chance: When's the last time you actually wrote a real
<meta> tag in HTML? I couldn't tell you. So, like, we can abstract that stuff.
[00:23:16] Ben: I mean, I live in Eleventy, so… yesterday.
[00:23:19] Chance: Okay. Fair enough. Well, for me, like, I don't know, before I was a React developer, I was a WordPress developer, so we just use, like, Yoast SEO and all that stuff.
[00:23:28] Ben: Sure.
[00:23:29] Chance: So I don't know. I don't touch
<meta> tags anymore, so I'm a little rusty. But it's nice, 'cause Remix actually teaches you a lot of things you've forgot, you know?
[00:23:36] Ben: Yeah!
[00:23:36] Chance: Like, you can only hold so much information in your head as a developer, I think. At least I can. I just… I chuck stuff away in the deep recesses of my mind, and then it just goes away. So I've had to relearn a lot of web fundamentals just by building on and using Remix.
[00:23:53] So anyway, so this route, we're exporting some meta. We're exporting some links. This link, we're just exporting a stylesheet for this specific route. We're exporting this action thing. The heck is an action? We'll come back to that in a minute.
[00:24:07] Ben: Okay. Gonna hide you, then.
[00:24:08] Chance: Just collapse that, yeah.
[00:24:09] We got our loader. We know what our loader does. So, loader is going to check to see if we have a user. So if we hit this route, but we're already logged in — if you hit the sign-in, right, but you're already logged in, then there's no reason to log in again. So it's just going to — same thing as our index route — it's going to redirect you to your dashboard if you have a user.
[00:24:29] Ben: Makes sense!
[00:24:30] Chance: And then we've got… we're exporting our default
SignIn component, and this is where our rendering actually happens, right? So we've got a few things going on. We've got this
actionData thing which, as you might imagine, comes from that action function we looked at before. We'll come back to that.
[00:24:46] Ben: Okay.
[00:24:47] Chance: We're pulling some things off of our action data. We've got this
useSearchParams hook. This is coming from React Router as well. Actually, we're importing it from Remix, but it's a React Router feature. Remix, if you don't know, is built on React Router. They handle all of this routing. So React Router version 6 is really the powerhouse behind a lot of Remix's core functionality.
[00:25:08] And then we've got this form ref and we've got this hook that I wrote to help us do some focus management. I love that this is an accessibility-focused podcast because I've tried to build it… I tried to build a really accessible application here. There's still some things I need to work on and improve, but we've got some decent focus management going on on each route, which I'm really excited about. So this is going to help us — if we have an error in our form, it's going to refocus the first form field that has an error.
[00:25:37] Ben: Okay.
[00:25:38] Chance: Which is, I believe, what the ARIA folks recommend us, or the W3C recommends that we do.
[00:25:44] And then we're just rendering some components after that. We're rendering the sign-in form. Aaand that's kind of of it. So let's talk about our action again.
[00:25:54] Ben: Yeah! Let me expand this.
[00:25:57] Chance: So, what the heck is an action? So if a loader function is handling our
GET request, our action handles any other type of request.
[00:26:06] Ben: Okay.
[00:26:06] Chance: So if you're doing any sort of mutation — any
PATCH, any other type of request — it's going to be calling your action.If you export that from that component.
[00:26:17] Ben: Mmm!
[00:26:17] Chance: If you don't export it, nothing happens. But essentially, we make a
POST request and we're going to call this action function.
[00:26:25] Ben: Okay!
[00:26:25] Chance: So how do we make this
POST request? How do we actually sign in? So when we sign in, we want to make a
POST request. We are
POSTing to our server, we're sending it some credentials, and we're saying, "Hey, server, deal with these credentials," right? We want to handle all that stuff on the server. We don't want no weird client-side stuff happening when handling sensitive information. So what we're going to do is, if you take a look at our component again — we are ultimately rendering… We're looking for a form.
[00:26:58] Ben: Like this.
[00:26:58] Chance: Yeah! That's capital "
Form," though. What is that capital "
Form?" That suggests we're dealing with a custom form component. Well, this is an import that we get from Remix as well. Remix exports its own form. It acts a lot like the native
<form> in HTML. It handles your requests. It has the exact same API that you're used to if you know how to use HTML forms.
[00:27:20] The difference is, is that we get to not only handle the form… So a traditional form, a plain HTML form, is going to send a request to the server and then do a full refresh of your page because you're changing routes when you post a form action. Or you're potentially changing routes. But it's always going to refresh the page, right? But when we're using React Router and we want to preserve some of the functionality of React Router and handle those client-side transitions — Maybe we've got some page transitions, right? Maybe we've got some transitions happening when data comes in. The Remix
Form gives us that ability.
[00:28:11] Ben: Mhmm.
[00:28:12] Chance: —they'll just fall back to a plain ol' HTML form and it still works, right? It's progressive enhancement, essentially.
[00:28:17] Ben: You don't get the nice transitions.
[00:28:19] Chance: Yeah!
[00:28:20] Ben: You don't get the, like, "Oh, everything doesn't jump around" or whatever. Like, you don't get that but, like, it's still functional. It still works behind the scenes.
[00:28:28] Chance: Yep.
[00:28:28] Ben: Yeah.
[00:28:28] Chance: Yeah, yeah, exactly.
[00:28:30] Ben: Progressive enhancement!
[00:28:31] Chance: So this is a progressive — yeah, exactly! This is a progressively enhanced form, and we're going to send a post request when this form is submitted. And once we do, since we don't — if you're not familiar with the Form API in HTML, since we don't
POST to a specific action, the default action is the route you're currently on, right?
[00:28:50] Ben: Okay!
[00:28:50] Chance: So we can POST to different routes. We could
POST to another route somewhere else in our route hierarchy, or we can
POST to this current route! And since we don't have a specific action, we default to the current route. So this form is going to be handled in our action function.
[00:29:04] Ben: Okay. And so if it
POSTs to itself, does that then mean that — 'cause we've got the, like, the redirect where, like, if you do have a user, it'll take you to the dashboard — so it's basically hoping… Like, it
POSTs to itself, so it would reload itself, and then hoping that the redirect catches it?
[00:29:29] Chance: Well, again, the loader is executed by
GET requests, right?
[00:29:33] Ben: Mmm.
[00:29:33] Chance: We're making a POST request. We've already checked with the user once we get the route. But right now, we're making the
POST, so we're going to call our action function.
[00:29:42] Ben: Okay!
[00:29:42] Chance: So we're looking at a different function here. So if we take a look at the action, this is basically where we start handling all that data that just came from our form, right?
[00:29:50] Ben: Gotcha.
[00:29:51] Chance: So, first thing we do is we await the form data that comes from the request. This is just the standard — web standard
Request object. And you can call
.formData(). This is an asynchronous function. And once we have that, this is essentially an object full of all of the fields that were in our form that was submitted, right?
[00:30:10] Ben: Mhmm.
[00:30:10] Chance: So we go ahead and call
formData.get() on the various fields that we have. We've got this email, this password, and a redirect location once we're logged in. And now we have the data. We've got all of our data that we need to work with.
[00:30:23] Now what we're doing is we're just going to validate that data, right? You always want to validate your data on the server. You might want to validate it in the client, too. For most folks who are in the React space, you might be used to libraries like Formik, right, that, you know, build these really nice, fancy client-side forms, and folks use various libraries for validation on the client of those forms. And that's fine! There's nothing wrong with that. But you know, the client also has — the browser has really great built-in validation as well that we can rely on, or we can handle client-side validation. But that's not always enough, right? Because a really savvy user can get around that. So we want to validate that stuff on the server, too, and send the correct response if we have invalid data, right? So we're going to validate all of those fields. So most of this logic is just validating, checking that we've got the right type of content here.
[00:31:11] And then once we finally validate all that and we've got the data that we need, we're going to start handling the login process. So we're still validating right now, and keep going down.
[00:31:21] Ben: There we go!
[00:31:22] Chance: We're setting up some errors. We're returning those errors if we have any. We're returning this
json function. What the heck is
json is also something we get from Remix and it is going to wrap our return value and return some data, but also a response for that data. So again, HTTP status codes. This is a
401 response, which I believe is just, like, an invalid thing. I can't remember. I have to, like, always look at MDN for the response codes.
[00:31:49] Ben: Yeah…
[00:31:50] Chance: But anything in the 400s is always bad. So we're returning a bad response here with our errors so that we can take those errors in the server — or in the client, rather — and render them, right? So that's where that
useActionData hook comes from.
[00:32:05] But once we've validated everything and we know that we're logging in, we're going to call this
createUserSession function that is going to redirect us to the dashboard once we've successfully logged in.
[00:32:14] Ben: Okay.
[00:32:16] Chance: Make sense?
[00:32:16] Ben: Yeah! There is a LOT going on, but yeah.
[00:32:19] Chance: [laughs] Yeah, I know!
[00:32:19] Ben: Makes sense.
[00:32:21] Chance: So, yeah, no, there is a lot going on. There's a lot to explain. I think once we start seeing this over and over in our routes, eventually it starts to click — I hope, you know — and once it does, I don't know, for me, like these conventions make building these kinds of apps so much more productive for me, so I'm really, really happy with it.
[00:32:42] So, anyway! This is our sign-in page, right? That's everything that's happening if you sign into our application. It's quite a lot, and there's still a few abstractions that we've got here that we don't really have time to get into.
[00:32:53] But everything in our action, just like everything in our loader, is only ever called on the server. Another really cool thing about Remix is that it automatically splits all that stuff out for you. So even though all of that code is in the same file, your action and your loader, all the code that is related to your action and loader are all split out of your ultimate client bundle. Our compiler does all that for you, so you don't have to worry about any of this action or loader code junking up your client bundle, which is really nice.
[00:33:23] Ben: Good deal! Man!
[00:33:24] Chance: That's a lot! I'm out of breath!
[00:33:26] Ben: So much for just one page, but it really does show how the stuff is all fitting together, so thank you.
[00:33:33] Chance: Yeah, of course. And so with that said, let's go to another page! Do it again.
[00:33:37] So we can't sign in yet because we don't even have a user account, do we?
[00:33:42] Ben: Right, no, I haven't created one yet.
[00:33:44] Chance: Why don't we do that? Yeah, register here. Let's click that. Now we're on the register route. Go ahead and fill that out.
[00:33:57] Ben: And then put in a dummy password here, and then… Oh, look at that!
[00:34:03] Chance: Oh man, what happened? "Password must contain at least 1 special character." Let's check out this route file next. Take a look our
[00:34:10] Ben: Okay!
[00:34:13] Chance: So, in our
register file, it's going to look very similar to our
sign-in file. We've got a
meta function that exports some meta about our page. We've got this
links function. We got a loader that appears to be completely commented out for some reason. I don't know if that's… well, I'm not worried about that. Like I said, this is mostly… this app is in development, so I wouldn't worry about that little guy.
[00:34:36] Let's take a look at our action. So just like our sign-in form, our action form is going to get all of our form data, it's going to validate that form data, and then it's going to return some data for, like, based on what happened, right? Or it's going to redirect us after we've successfully created a user.
[00:34:53] And we note here that we've got this
fieldErrors object. We've got this
validateEmail function. That
validateEmail function is going to have all the logic necessary to validate that that's a correct email before we start serving it, right? We got the
validatePassword function, right? And this is what's catching your password problem, right? "Passwords must be at least 6 characters long," "Password must contain at least 1 special character." We could add any other validation or special rules that we want for our passwords.
[00:35:22] But I just wanted to just show you how that validation is happening on the server, not on the client, because again, if you have a savvy user who can open up dev tools, you can get around client validation. So this is all happening on the server. We return that data to the client so that you can see that error with the
useActionData hook. So if we have that action data, then we're going to get those objects in our route component!
[00:35:49] Ben: Okay, so… yeah, so we call
useActionData. We get those field errors.
[00:35:57] Chance: Mhmm.
[00:35:58] Ben: And let's see. Just don't do anything — this is a
useEffect, so we don't do anything if there's no errors whatsoever.
[00:36:08] Chance: Yeah, this is actually what that abstracted focus management hook that I showed you in the last route was actually doing.
[00:36:16] Ben: Got it.
[00:36:16] Chance: So this is a
useEffect, so this is happening on the client. And this is just because, again, we have to deal with client-side routing.
[00:36:23] Ben: Mhmm.
[00:36:23] Chance: And what we want to do when we reroute on the client, or when any of our form errors change, is we want to focus that first form field.
[00:36:31] Ben: Gotcha.
[00:36:32] Chance: So, you don't have to go through the nuts and bolts of this…
[00:36:35] Ben: Yeah.
[00:36:35] Chance: …but that's ultimately what it's doing, is checking to see if we have any form errors and then focusing the first field that it finds with an error.
[00:36:40] Ben: Okay! Makes sense.
[00:36:42] Chance: Yeah. But one thing you will notice — one thing that I notice about this route, there's not a single
useState. There's no state in this route. But we DO have state in this route! Right? We've got error state! We've got the validations there, right? We've got some state, but it's all handled by our server and it's just returned to us as soon as we load that route. But we don't have to call
useState. We don't have to do any fancy data fetching in the component itself. We don't have to juggle the effects or anything like that. We've just handled it on our server and it comes to us as soon as we get that data back.
[00:37:18] Ben: So then in that case, have you found that there's, like, EVER a use case for using something like
useState in a Remix application?
[00:37:29] Chance: Oh, yeah!
[00:37:29] Ben: Okay.
[00:37:29] Chance: Yeah, we'll see some later on. There is certainly a use case for
useState. We're going to talk a little bit later on about optimistic UI, and with optimistic UI, anytime you've got an optimistic UI, you're going to — generally speaking — you're going to have some sort of state somewhere, just to hold, like, the temporary state in memory while we're waiting on that state to come back from the server, right? So there are situations where you certainly need some state in your route components. But I find, at least, when I'm building Remix routes, like using Remix for me in some of the apps that I've refactored has eliminated, like, 80% of my
[00:38:12] Ben: Interesting!
[00:38:13] Chance: And the stuff that you DO use it for is mostly progressive enhancement!
[00:38:17] Ben: Okay!
[00:38:19] Chance: Yep. So, that's what we're looking at here. That's what happens when you register your user. So let's go back and check out our browser. Let's fix that busted password. Try again.
[00:38:30] Ben: There we go, okay.
[00:38:31] Chance: And here we go! We have signed into our application.
[00:38:34] Again, I have not spent a ton of time styling this thing so it's not, you know, it's pretty yet. But, you know, the layout works alright! So we've got this dashboard view, this greeting that says, "Hello, here's what you missed while you're away."
[00:38:48] So, we're building a project management app. So let's take a look and see how some of this stuff works. We've got this — Right now, we don't have any projects. Let's create a new project.
[00:38:58] Ben: Alright! "Some Antivs," apparently.
[00:39:26] Ben: Yeah!
[00:39:26] Chance: So, again, progressive enhancement. There is a little state there.
[00:39:30] Ben: Okay.
[00:39:30] Chance: But we don't need to add members, 'cause we actually don't have anybody to add 'cause we've only created one user! If we created more users, we could add some of those to our project, but we'll just go ahead and create this for now.
[00:39:40] Ben: Alright.
[00:39:42] Chance: And here's our project dashboard! There's your title. There's your description. There's a list of your users, which is currently only you. There's a little dropdown menu button on the right-hand side that we've got the ability to edit or delete the project. So we just got some basic CRUD functionality here! There you go. "Do it well!" So when you update, that is going to send a request to our server. Once the form is submitted, the UI in the background will only update after that request goes through.
[00:40:14] Ben: It doesn't actually seem to be doing it! Uh… let me just check the console real quick.
[00:40:20] Chance: Oh, okay, we got a bad route! Yeah.
[00:40:23] Ben: Okay.
[00:40:23] Chance: Unfortunately, I made some changes before the stream, so I'll go back and fix that, but good to know. I'm going to put a todo on my list because that, we are not going to fix today.
[00:40:34] Ben: Okay!
[00:40:36] Chance: "Fix project action." Alright, well. You know, I hoped that would've worked. That would've been cool, right?
[00:40:43] Ben: Yeah!
[00:40:44] Chance: So anyway, these things are live, right? We do it live.
[00:40:49] Ben: Yep!
[00:40:49] Chance: So now we need to create some todos for our project. It's not really much use without anything to do.
[00:40:55] Ben: Alright.
[00:40:55] Chance: Go ahead and do that.
[00:40:56] Ben: So I create a new list.
[00:40:58] Chance: Yeah, let's create a list.
[00:41:02] You gave me some really good feedback earlier on the UI here, and I can make some improvements to the "Add todo" button here, because it's not immediately obvious that this is actually a button. So I need to fix that! This is… it's also on my list of my own todos. Let's go ahead and click that "Add new todo."
[00:41:21] Ben: Real quick, we do have a question in the chat—
[00:41:23] Chance: Yeah!
[00:41:23] Ben: —that's from edmbn which is, "Do we have the code available on GitHub?" Not quite, because you're still doing some polishing. But when it is available, where would be the best place to find it?
[00:41:33] Chance: Yeah, so it'll be in the Remix repo. I'm also going to have my own repo. Before we put it in the Remix repo, I'll go ahead and create my own just to get it up faster, because we're going to put it in the Remix examples directory eventually and then start building onto it. But I'll have that code ready for everyone this week, and, Ben, I'll give it to you, too, so we can share it to your audience.
[00:41:55] Ben: Sounds good.
[00:41:56] Chance: But it will all be open, yeah. For sure.
[00:42:00] Ben: So, yeah, thank you for your question. Unfortunately, the answer is just not quite yet, but that's just because this repo needs to be even better to share it.
[00:42:12] Chance: It'll be ready this week. Promise. Pinky promise.
[00:42:16] Ben: Alright!
[00:42:17] Chance: Let's create that list!
[00:42:19] Ben: Let's do it!
[00:42:20] Chance: Boom! So we got a todo list. If click on that, then we can actually see some action items for our list. It tells us what project we're in, and we've got a list of todos that we can click around on and check out, and once we've clicked on them, we can also delete them if we wanted to.
[00:42:38] Ben: Okay!
[00:42:38] Chance: Yeah, so that's our todo list now. So let's go and actually take a look at this todo list. So if we go back to our code editor — well, first of all, before you do that, check out the URL.
[00:42:48] Ben: Okay.
[00:42:48] Chance: We've got this URL, /dashboard/todo-lists slash big old blob of text. What the heck is that? We should probably make that a prettier URL, but it is what it is. That is our dynamic ID for this todo list, right?
[00:43:03] Ben: Okay!
[00:43:03] Chance: This is the unique identifier that tells us which todo list we're looking at. So let's go back and figure out how we get to that.
[00:43:11] Ben: Alright.
[00:43:11] Chance: So in our
routes/ directory, we've got the dashboard, right? We noticed we have a nested URL. The URL structure being nested mirrors our
routes/ directly, right? We've got these nested routes. So, think of the
routes/ directory hierarchy is mapping to our URL structure. So "dashboard" slash "todo-list" slash dynamic ID slash nothing, right? That dynamic ID is the noted in our
routes/ directory by this dollar sign.
[00:43:39] Ben: Okay.
[00:43:39] Chance: So because this directory called
$listId/ starts with a dollar sign, that's now a dynamic route segment. So anything that we… If we have a dynamic route segment and we don't actually see a specific route that matches that segment, it's going to be assumed to be dynamic. So that's where we're getting that list ID, right? And that's available in our route as a route parameter. Plus, if we take a look at the index route inside of
$listId/, the index route is the index for that specific route. So when we don't have an additional route segment after that, we're assumed to be on our index, right? You don't need an index in a directory, but if you have multiple nested routes inside of
$listId/ like we do, we're going to have an index route.
[00:44:25] Ben: Does the route itself get the…? I saw it in the action.
[00:44:32] Chance: Mhmm!
[00:44:32] Ben: So action gets a
[00:44:35] Chance: As does our loader!
[00:44:37] Ben: And
listId. Does the route component itself get the list ID?
[00:44:43] Chance: It could, but we don't actually need it in the route component—
[00:44:46] Ben: Okay.
[00:44:46] Chance: —just because we don't see it. And I'll show you, because all we use that segment for is to fetch the todo list and the project.
[00:44:55] Ben: Gotcha.
[00:44:55] Chance: So if we remember, when we go to our URL, we call our loader function. So let's take a look at our loader function and see what's happening.
[00:45:03] Ben: Get rid of a few of these distractions. There we go!
[00:45:07] Chance: Yep, so inside of our loader function, we're getting that list ID from the params, that comes from our route params.
[00:45:12] Ben: Okay.
[00:45:12] Chance: And it's on the
listId key because that's what we've called it in our route with that dollar sign, right? If you're a PHP old-school developer like myself, you recognize the dollar sign as a variable, right? So we call it a dollar sign in the route, and that becomes the key for our params.
[00:45:28] So we get that list ID. We require a user on this route. We want to authenticate, and if we're not authenticated — so this is how you would authenticate in a route, you would call a function that checks your user session on the server, and if you're not authenticated, you would want to redirect them somewhere else. This is just a light attraction over that. But we're requiring our user, and then we're going to start fetching our information about our list and our projects. And that's where this list ID comes in.
[00:45:56] So we call this. We wait for all of these promises to resolve. This makes all of these requests concurrently, which is really nice. This reduces our network waterfall that you're going to see in a lot of websites where it sort of has, you have to call one function and then wait, and then call another function and then wait, and call another function.
[00:46:13] Ben: Yeah!
[00:46:13] Chance: We can call all of our async functions concurrently with
Promise.all, and once all of those are resolved, we've got our data and we can move forward with our loader.
[00:46:22] Ben: Yes, 'cause, like, in a lot of those applications where you've got that kind of waterfall, it would be something like, "Oh, a user's trying to access this todo list. Well, first we have to fetch the user so that we know, like, what projects they have access to. And okay, we need to now fetch their projects. And now that we've got their projects, we need the fetch the todo list." And you've already got this, like, chain because you can't just hop directly to the todo list because you have to make sure, like, every step along the way is working.
[00:46:50] Chance: That's right. And there's no magic involved in that because we're just using the platform. We're just using native fetch and we're using Promises, right?
[00:46:57] Ben: Mhmm.
[00:46:58] Chance: So there's no Remix magic here other than the fact that Remix completely relies on the platform. It gives us a lot of power, right? We're relying on… in this case, we've got a Node server. Remix also supports non-Node targets like Cloudflare Workers, which is really cool. We're gonna support Deno very soon. So it's not just Node. But we're going to support pretty much any platform that is wanting to support native platform functionality like fetch. So, pretty cool, I think, that we get to do all this stuff basically for free because it's what the platform gives us!
[00:47:27] Ben: Mhmm. Very cool.
[00:47:29] Chance: So, yeah, once we fetch our todo list and our projects, we're going to make sure we actually got a todo list back from our database request. If we don't, we'll just redirect them. We could also handle an error there if we wanted to. This was just out of convenience for me. But we're going to check to see if we have a project associated with our todo list. If we don't have that project, again, we're just going to redirect them.
[00:47:51] Ben: Mhmm.
[00:47:51] Chance: You could handle that error differently if we wanted. But then once we have the project associated with that todo list, as well as the list itself, we're going to return it as data that we can access in our component!
[00:48:01] Ben: Okay!
[00:48:03] Chance: And that's where we get all of the information that we're going to render!
[00:48:05] Ben: Makes sense!
[00:48:07] Chance: So if we go down to our component again, we're going to see everything from that
useLoaderData function. Right?
[00:48:15] Ben: Got it! So I can start to do stuff with todo lists like iterate over…
[00:48:20] Chance: Yeah!
[00:48:20] Ben: Okay.
[00:48:21] Chance: So you also see this
useFetchers hook. What the heck is that? So let's talk about that.
[00:48:26] Ben: Okay!
useFetchers is… so we talked a little bit about the
Form component already, right? Remix exports a
Form component to allow you to make a
POST request just using a plain ol' form with some progressive enhancement, right?
[00:48:41] Ben: Mhmm.
[00:48:41] Chance: What happens with a form — this is just how the HTML
<form> works by default — is it's going to
POST to a specific URL, right?
[00:48:49] Ben: Yeah.
[00:48:50] Chance: And what it's going to do when you get that data back is it's going to return that URL. You're going to go to that URL, right? So if you're POSTing to any other route other than the route that you're on, you're gonna navigate to that route. You can think of a link, an anchor tag, as just a fancy form essentially, right? It's a form that posts to the route in your href, and it's posting a
GET request. It's actually a GET request, not a POST request.
[00:49:13] Ben: Okay!
[00:49:13] Chance: But it's really just a fancy form that's a little simpler to write.
[00:49:16] But what
useFetchers is, is sometimes you want to make a request to a different URL, but not actually navigate to that URL. You just want that data back.
[00:49:28] Ben: Gotcha.
[00:49:28] Chance: That's where
useFetcher comes in. And we've got
useFetchers. They're two separate hooks. We'll talk about those more specifically in a moment. But
useFetchers is going to… you can have multiple fetchers in the same route, and they might all be inflight. They may all be fetching some data at the same time. And you might want to handle independent fetchers, and you might want to handle them in a specific order. You might want to deal with race conditions. You might want to do a number of things with your various inflight requests, right?
[00:50:00] Ben: Yeah.
[00:50:00] Chance: Before they actually get back. You might want to show some pending UI. There's a bunch of stuff you might want to do. Again, what would we normally do to deal with data loading? We'd use state! We'd use an effect, right?
[00:50:13] Ben: Yeah!
[00:50:14] Chance: We don't have any state in this app, right? Even though we are doing stuff that technically feels stateful, all of that state is still coming from the server.
[00:50:22] Ben: Okay.
[00:50:22] Chance: So that fetcher is going to have some data attached to it. It's gonna tell you what's the type of that fetcher, right? The fetcher that we're dealing with in this particular moment — we'll show where that actually that magic actually happens in a minute — but the fetcher that we're dealing with now is dealing with a form submission that is going to update our todos, right? And we actually, because we have multiple fetchers, we could be dealing with different types of fetchers. We could be dealing with submissions that are submitting to our
/edit endpoint, which is gonna update the details on our todo. We could have submissions posting to our
/new endpoint, which is going to create a new todo item, right?
[00:51:02] Ben: Okay.
[00:51:03] Chance: We could be posting to our
/delete endpoint. So we can tell exactly which fetcher we're dealing with based on the action that we posted to. Right?
[00:51:13] Ben: Iiiinteresting.
[00:51:14] Chance: Does that make sense?
[00:51:15] Ben: Okay, okay.
[00:51:18] Chance: So, we look at all of the fetchers that we've got inflight, and we can manipulate data based only on those fetchers. And what we can do is we can essentially derive state in our component from data from the server! So we don't actually need to set any state. We just create some objects that are created on render, and they render whatever data comes from our server.
[00:51:40] Ben: Where… where are we doing that?
[00:51:41] Chance: And It can happen synchronously.
[00:51:43] So what we're doing here in this loop is we're effectively getting all of our todos that we want to render. And the reason we're doing this, instead of just getting
todoList.todos and rendering whatever came from the server—
[00:51:55] Ben: Mhmm.
[00:51:55] Chance: —the reason we're doing it this way is we actually want to optimistically update this UI so that the interactions feel instantaneous. So we're not actually going to get that data back as soon as you click that button, if we're hitting a real server. Right now, it's all local, so it's going to be pretty fast anyway.
[00:52:12] Ben: Mhmm.
[00:52:12] Chance: If you're dealing with a real server, it might come back slow! You might have a slow connection and somebody might be waiting a few seconds before they get the data back, right? But you want your app to feel really nice and snappy, right? So we, we call this optimistic — We don't call this "optimistic UI." EVERYBODY calls it — this is just what it's called, right? Like, we didn't invent that term. But this is how you would deal with optimistic UI in Remix! You would derive the state from the data that you get based on the transition from—
[00:52:36] Ben: Okay!
[00:52:37] Chance: —that fetcher. So what state is that fetcher in right now? If the state is "submitting" — if we're busy — in that for loop, you can see that we're checking to see what state our fetcher is in, and if we're in a certain state and we know we're busy, we can go ahead and update the UI before that data ever comes back. And if the data doesn't come back or we get an error from the data, if there's a problem, we can handle that in our catch boundary. We can render a catch boundary here and handle that in the UI directly.
[00:53:08] Ben: So I'm trying to figure out where that actually happens, though. So I see… like, where are we displaying…?
[00:53:18] Chance: So scroll back up for a moment. We'll just walk through the loop real quick, 'cause the loop is actually doing a bunch of things. So we're looking at all of our fetchers, but we've got this… So, the one that I'm talking about primarily right now is this
[00:53:34] Ben: Okay.
[00:53:34] Chance: So, right before the loop starts. We're creating an array of todo items. And this is initially just whatever we have from the server, right?
[00:53:43] Ben: Mhmm.
[00:53:43] Chance: Whatever we have from the server right now is our todos. And if our… So, scroll back down. So the second
if statement is actually the one that I want to talk about.
[00:53:55] Ben: The "new."
[00:53:55] Chance: So if we have a submission — so with
fetcher.submission… so first of all, if there is a submission at all. If there's not a submission currently happening, there would be no fetcher submission. So this only happens if there's a submission. So if there's a submission currently inflight and the action is posting to create a new todo, right? This "new" action is intended to create new todos.
[00:54:22] Ben: Mmmm.
[00:54:22] Chance: We're going to optimistically update our todos with this temporary derived state, right? This is just like in-memory state, right? This is only happening as we render the component. But once we get that data back, we're going to call
render again on the whole route component.
[00:54:38] Ben: And that time, it's gonna get the list of all the ones from the server including the brand new todo that's been finalized in the database. Okay!
[00:54:44] Chance: Exactly.
[00:54:45] Ben: Got it, got it.
[00:54:45] Chance: And if our server throws an error, we can deal with that error in a catch boundary.
[00:54:49] Ben: Got it.
[00:54:49] Chance: But right now, we're optimistically updating our UI before we actually get new data back from the server.
[00:54:57] Ben: Got it.
[00:54:57] Chance: And we don't have to have any state for that.
[00:54:59] Ben: And so here, we're using
allTodos which is our todos we got from the server plus the todos that are currently inflight on their way to the server. Got it.
[00:55:09] Chance: That's exactly right. And this all… I know this looks really weird, right? We're all like, "What are we creating this, like, mutable array for?" and like "This is not the React that I know. This is not the immutable React that I know and love," right?
[00:55:23] Ben: Mhmm.
[00:55:23] Chance: It takes a little bit of a paradigm shift. But the point is that we can update the UI and create an app that feels really, really snappy. And another really cool thing about the fetchers API is that all the race conditions that you might imagine happening as you're clicking todos really, really fast and you've got the server responding slowly… Remix handles all that for you. So you don't have to worry about user clicking so fast that your server can't handle the… We've got, like, a jillion tests to do with all kinds of weird race conditions. So, it's actually really cool. We don't have time to get into the nuts and bolts of all that stuff, but it's pretty cool how that stuff works under the hood. And it's all open source, so if you are really fascinated by that kind of thing, you know, go jump in. Take a look.
[00:56:08] Ben: Alright!
[00:56:09] Chance: But yeah, we've got multiple fetchers because we are dealing with
POSTs to our
/edit endpoint to update a todo, our
/new endpoint, and our
/delete endpoint. We're not going to go through every single one of those, but that's what's happening in that loop. We're looking at all of our fetchers and deciding what to do based on what to render based on what that action currently inflight is giving us!
[00:56:31] Ben: Okay! Got it. Very cool.
[00:56:36] Chance: You got anymore…?
[00:56:37] Ben: Yeah, like there's a lot going on, but it's very cool to see how the stuff all fits together. Yeah, so I guess MY grand summary of what I've learned so far: Loaders, that's us getting data from the server. Actions are mutating the data the server has access to, so I would almost think of that as, like, in GraphQL land what "mutation" covers, right?
[00:57:04] Chance: Yep, exactly.
[00:57:05] Ben: Fetchers are for the stuff that's currently inflight that, like, hasn't quite been persisted in the database yet but we want to update the UI to make it look like it totally has so everything everything seems a lot faster regardless of network connection speeds. Okay.
[00:57:23] Chance: Yeah. Caveat — the only caveat in the fetcher thing is that they're… Think of fetchers if you're making a request to a different URL that you don't want to ultimately navigate to.
[00:57:33] Ben: Oh, okay, got it.
[00:57:35] Chance: If you do want to navigate to that URL, you can use our plain ol'
Form component. You can also create a plain HTML form, and we also expose a hook called
useSubmit. And those APIs are actually quite a bit simpler than the fetcher API, but the fetcher API is really handy if you want to POST to an endpoint that you don't want to navigate to and you just want to handle that data right there in the same route. And if you have multiple things going on at once, again, you can use all of those fetchers to get all of the data associated with that request. So, that's the one caveat. 'Cause I would generally recommend if you're just, you know, posting to a different URL and you plan to navigate to that URL later like we did with our sign-in form…
[00:58:17] Ben: Mhmm.
[00:58:17] Chance: I would use the Form API. But all of that, yeah, I would take a look at the Remix docs for both the Form API, the
useSubmit API, and the
useFetcher API and read through the nuances in those.
[00:58:28] Ben: Okay. Hm! Lots of stuff to get this up and running, but are there more things you wanted to show, or things you wanted to add to this?
[00:58:40] Chance: Yeah, I think we can move on from this, because I actually want to get to what our project is today, right? Like, we've talked a lot, right? We've been on this thing for an hour now. We've done a lot of talking. Tried to explain everything as best I can in a one-hour situation. But we want to build some stuff, too.
[00:58:58] Ben: Yes.
[00:58:58] Chance: So one thing that I really want to do is I want to… So, if you hit the Back button real quick in the browser. Let's go back to…
[00:59:07] Ben: Chrome's doing this thing where it, like, pops down. It's weird. Okay, so I hit Back.
[00:59:11] Chance: No worries. So what I really want to do is, instead of, when I click the todo list, instead of navigating to a separate page, I would love it if that todo list just appeared on the right-hand side of the same page, right?
[00:59:24] Ben: Okay.
[00:59:24] Chance: I don't actually want to change my view here. I want to use the same layout for this just to see my todo list. I want to see it right there on my project dashboard. So what I'm going to do here is instead of navigating to this todo route, I'm gonna create — or, I'm gonna have you create a new route!
[00:59:41] Ben: Okay!
[00:59:42] Chance: So let's go over to our code, and pop open your
app/ directory and pop open its
routes/ directory. And inside there, you're going to go in your
projects/ directory. And inside there, you're gonna open up the
[00:59:57] So what I want here is I want my URL to be
/dashboard/projects/$projectId/list. So create… let me see here. What did I ultimately do here? I think I created a
list/… Just create a directory inside of the
$projectId/ directory and just call it "
list/." Just plain ol' L-I-S-T. And inside there, create a new file, and call it "
$listId" — use camelcase — and
.jsx. Doesn't matter. We'll keep it TypeScript, and we'll do work with some TypeScript. So, now what I want you to do is I want you to go into the… back up a bit and go into your
todo-lists/$listId/ folder there, and I want you to open up that index file.
[01:00:57] Ben: Okay.
[01:00:57] Chance: Now I want you to just go ahead and copy all that code.
[01:01:01] Ben: Got it, okay.
[01:01:02] Chance: Copy the whole route, and then paste it into that new file you just created. And then go ahead and press Save. And let's just start here.
[01:01:11] Ben: Okay.
[01:01:12] Chance: So now let's go back into our
$projectId file, that dynamic
[01:01:21] Ben: This one? Right here?
[01:01:24] Chance: That's the one, yeah!
[01:01:26] Ben: Okay.
[01:01:27] Chance: And so what I want to do here is I want to do… So, what we have essentially done just now is we've created the nested route, right? And we talked earlier about, you know, when we looked at our root file, right? We had this root file that rendered that
<Outlet />. Remember
[01:01:46] Ben: Yeahhh. Wait, let me pop that back open and… you go ahead.
[01:01:51] Chance: Yeah, so
<Outlet />… you can have more than one outlet in your route hierarchy. And the way the outlet is going to work if you've got a second outlet nested inside is it's going to handle all of your routes that are nested inside of your current route. So we can create a nested route UI based on the routing structure, and we can render whatever is nested inside of our project ID through another outlet.
[01:02:19] Ben: Okay.
[01:02:20] Chance: And so that's exactly what we want to do. If you just jump back to the browser real quick, we've got this space — well, we don't have a space yet, but we want to create a space right next to our todo lists where we render our actual todo list, and that's going to be our new route.
[01:02:35] Ben: Okay.
[01:02:36] Chance: And it's just going to stay right there in the same UI. We're just going to pop it in a separate
[01:02:43] Ben: Makes sense.
[01:02:43] Chance: So in our $projectId, I'm looking for… what I want you to look for is we've got this
<Layout> component. It's got this
main prop that actually handles our… that's where all of the todo lists are rendered. This is kind of a big, chunky component. But you can collapse all that stuff. What I actually wanted to do — collapse the
<div> right underneath "flexer."
[01:03:10] Ben: Right underneath what? Okay.
[01:03:11] Chance: Yeah, that guy. So right after that, I want you to just render an
<Outlet /> component. Yeah.
[01:03:23] Ben: Has this been pulled in already?
[01:03:25] Chance: No, we're going to need to import that, so add that import from Remix at the top of your file.
[01:03:31] Someone says that you work at Microsoft, so you're contractually obligated to use TypeScript.
[01:03:36] Ben: You know, there's some truth to that.
[01:03:39] Okay, cool, so, we've got an
<Outlet /> now.
[01:03:42] Chance: Yeah, so this is going to render our outlet responsible for handling all of our nested routes. So the next thing I want you to do is, there is a link that is linking to our todo list. I think it's on line 243?
[01:04:01] Ben: Which of course, you didn't have to look up, but you just kind of knew.
[01:04:05] Chance: Yeah. I know all the code that is on every single line in every project that I worked on. It's one of my underlying skills.
[01:04:17] Ben: Sorry, so what am I looking for?
[01:04:19] Chance: So, right up there. So, it's actually 237 in yours. We've got some…
[01:04:23] Ben: Okay.
[01:04:23] Chance: I must've changed the code. Yeah, so… This
<Link> component that we use also comes from Remix. And the way that
<Link> is going to work, again, mirrors React router, because we're really powering all of our routing with React Router 6.
[01:04:39] Ben: Mhmm.
[01:04:40] Chance: This "
../" convention… React Router uses nested routing, and we're also able to link relative to our current route. So these relative links… You can link relative to your current route just the same way you would imagine
cd-ing into a different directory.
[01:04:56] Ben: Mhmm.
[01:04:56] Chance: The "
.." syntax is just going to back you up to the next route level up. And so we're currently linking to
/todo-lists/$listId. We actually want to link to
list/ without the dots at all and without the initial slash. So go ahead and remove the dot and the initial slash, and just say
[01:05:15] Ben: Oh, yeah, okay.
[01:05:20] Chance: Yeah, so now, where is this going to link? It's going to link relative to your current route, which is your project ID.
[01:05:25] Ben: Okay.
[01:05:26] Chance: The
[01:05:32] Ben: Putting that to the test now.
[01:05:34] Chance: Yeah.
[01:05:34] Ben: We'll see what changes, if anything. Oh, oh… Okay!
[01:05:41] Chance: Why did it show up there? Because that's where our outlet is!
[01:05:44] Ben: Okay! And I'm looking at the URL there and yeah! Okay! So it just works!
[01:05:51] Chance: Pretty cool, then.
[01:05:52] Ben: That's awesome
[01:05:52] Chance: So that's all there is to it, right? Now, you can style that and do whatever you need to do to make it look pretty.
[01:05:58] Ben: Sure.
[01:05:58] Chance: Which I would probably recommend, 'cause right now it's a little… little cluttered, but we could clean that up a little bit.
[01:06:03] Yeah, so now we've got a nested route. Yeah, go ahead and create some more todo lists. Click around and see them change. And they're only going to change in that part of the page.
[01:06:20] Ben: Okay, so, alright. I open up Action Items, and it pops up over there. Very cool. And then I open up List 2, and it pops up over there.
[01:06:31] Chance: Yeah, we've also because of our scroll restoration… we would probably want to make some changes to the scroll behavior in an nested route situation like this, just for a little bit better user experience here, but we don't really have time to deal with scroll stuff today.
[01:06:46] Ben: Sure.
[01:06:47] Chance: So we can revisit that in a future episode, perhaps. But yeah, we've got a pretty good nested UI here though.
[01:06:55] Ben: Alright!
[01:06:56] Chance: Pretty happy with it.
[01:06:56] So, one more thing I'd like to do is, we've got this list over here on the right — we can check todos, either complete it or we can delete them — but I do notice that we can't actually create new todos on our lists, which is not really useful, so…
[01:07:11] Ben: Okay!
[01:07:12] Chance: Let's do that next. Let's go into the new component that you created, the new route component inside of
/projects/$projectId/list/. Probably don't want that nested a URL, but eh! So what we want to do here is we want to create a UI for creating a new todo.
[01:07:39] Ben: Okay.
[01:07:39] Chance: So, below your route component… Really anywhere, doesn't really matter, but somewhere in the root of the file, we want to create a new component.
[01:07:48] Ben: Okay.
[01:07:48] Chance: And I want it to function…
[01:07:50] Ben: I'll just stick it at the very bottom.
[01:07:51] Chance: Yeah, totally fine. Call it
[01:07:56] Ben: Okay!
[01:07:57] Chance: And it's going to take some props, so let's go ahead and give it some props. You can destructure it or not. Completely up to you. And let's give it a
listId prop. Since we're using TypeScript, it's going to yell at us for not typing this prop. So, no, actually, so… your syntax is a little off here. Let's go… so, since you're destructuring the object, you're actually going to put the type definition after the closing bracket, and this is going to be — so create a new object. There you go. Now say
listId: string, but lowercase S.
[01:08:31] Ben: Lowercase!
[01:08:32] Chance: Yeah, 'cause
String capital is going to be the constructor for a string object, which you don't want.
[01:08:41] So yeah, so this is our
NewTodoForm. And so go ahead and before we return — so what we're going to return here is we're going to return a form that we get from a fetcher. So we're going to explore how the fetcher… We never actually got to what the fetcher was actually submitting. So we're going to do that now.
[01:09:00] Let's go ahead and get a fetcher by calling, we'll say, a new variable called
todoFetcher. And we will call
useFetcher, which we're going to import from Remix, which I think is already imported in this component.
[01:09:16] Ben: Looks like it.
[01:09:18] Chance: And we are going to create some state here, and the reason I want to create some state is because we've got a form field that we want to clear after we put some data in that, and we do want to hold that in state because it's just easier to clear that form field in state. You could clear it by updating your key after submission, but that also clears focus and we don't want to do that. So I'm just going to use state.
[01:09:39] Ben: Okay.
[01:09:39] Chance: We do need state sometimes. So called this
setValue. And initialize with an empty string.
[01:09:48] Ben: You got it! Oh, I guess—
[01:09:50] Chance: No, yeah, yeah, initial state. There you go.
[01:09:53] Ben: Look at me, promising I know React!
[01:09:56] Chance: That's okay. We'll get there. And let's go ahead. Let's create a new variable and call it
submissionAction. And get it from… This is going to come from your
todoFetcher.submission. And if you have — remember, I said earlier — if you have a submission at all… So, submission's an object. If you have one, it means you're inflight. This fetcher, there's some fetcher inflight, or this fetcher's inflight. So if this submission is defined, it'll be inflight. But we actually want to get one more property on it, so you can use optional chaining syntax if you want, and call
.action. 'Cause we really only care about the action itself.
[01:10:39] Ben: Okay.
[01:10:39] Chance: So now let's create a… Let's leave this. Let's come back to this, actually. So go ahead and return what we're going to render. And we're going to render… so, open a new JSX tag and call
todoFetcher. There's actually a component attached to
Form. And that's what we're going to render.
[01:11:01] Ben: Okay…? Interesting.
[01:11:04] Chance: And again, this is just like an HTML form, except the only difference is we're not going to navigate to this route after we submit the form.
[01:11:11] Ben: Okay.
[01:11:39] Ben: Okay.
[01:11:39] Chance: 'Cause then it would just be a plain ol' HTML form. But in our case, we don't want to want them to navigate so we're going to add a progressively enhanced feature via the fetcher to handle that for them.
[01:11:48] Ben: Interesting.
[01:11:49] Chance: So we're going to have some props in our form, too.
[01:11:51] Ben: Okay.
[01:11:52] Chance: So let's define some props. Yeah.
[01:11:54] Ben: Cool.
[01:11:57] Chance: So in our props for our form, we're going to have an action, because we're posting to a different URL. That action is going to… it's actually going to be a string and it's going to start with "
dashboard" — "
/dashboard." You actually have to have the root slash here because we don't… I don't want to do relative URLs. We're in the nested structure here, so we're just going to do an absolute. So "
dashboard/todos/new." Why don't you open up that route file real quick, so we can see what that's actually going to do?
[01:12:26] Ben: Yeah!
[01:12:26] Chance: Because this request is going to go directly to that route.
[01:12:30] Ben: "
[01:12:33] Chance: "
[01:12:35] Ben: Okay.
[01:12:36] Chance: So it's really easy to know exactly what we're posting to.
[01:12:39] Ben: Yeah.
[01:12:40] Chance: So, what we're posting to is going to call our action, so we're interested in the action this route exports. So again, this is a lot like our sign-in form…
[01:12:50] Ben: Okay.
[01:12:50] Chance: …where we get the data from the form. We handle it in the action. We validate it, make sure it's all good data. And then if we ensure that we have good data, we respond with the actual todo itself. If we don't have good data, we respond with whatever error we want to allow the route to handle. So you can think of this as like an API route. We call them "resource routes" in Remix. This route is only responsible for giving us a resource. We post to it. It gives us a resource. There's no UI associated with it.
[01:13:21] Ben: Oh!
[01:13:22] Chance: There's no component. It's only handling requests. So this is an API route, and you can return any data you want from an API route or a resource route. You can literally — as long as it's serializable and you can send it across the wire, you can return it.
[01:13:36] Ben: Okay.
[01:13:36] Chance: So you can return static CSS this way. You could return files this way. There's all kinds of things you can use resource routes for. In this case, we're using it to post to our server to create a new todo.
[01:13:49] So we can go back to our $listId component and finish wiring that up. We want to… Go ahead and give it a classname. I've just got some basic styles I want to add here.
[01:14:02] Ben: Sure.
[01:14:02] Chance: We've just got some utilities. Go say "flex," and then another one for "flex-col." I stole these from Tailwind. And then another one that says "gap-4." We're just gonna put some things in here in a little flex column stack.
[01:14:23] Ben: Make them look nice.
[01:14:24] Chance: And then we want to say
method="post", because if we don't, the default method is a "get." We're not handling
GET requests. We're handling
POST requests! There you go! I think it's the only lowercase actually, in this case, yeah.
[01:14:38] Ben: Okay.
[01:14:39] Chance: And that's just easier to be consistent when we're dealing with them in the actions.
[01:14:45] We also — so, inside of our form, let's go ahead and render a hidden input,so
<input type="hidden" />. This is just going to hold our list ID. So go ahead and say,
name is equal to
"listId" — camelcase — and then the value is going to be our list ID that we passed via props.
[01:15:09] Ben: Okay!
[01:15:11] Chance: And then we're going to… I've got some minor abstractions that I'm going to use. Go ahead and render below this… I'm going to call it
FieldProvider. And you're going to import this from our
ui/ directory. This is just a… Look at that. TypeScript knows what to import.
[01:15:27] Ben: Yep.
[01:15:28] Chance: I think it's VS Code doing that magic. But anyway, in our
FieldProvider, give it a name, and it's just going to be the string "
name." This is going to be the name of our todo.
[01:15:37] Give it an ID, and call it
newTodo. This is just so that the label knows what it's associated with for accessibility. We're gonna make this a required field, so let's say
[01:15:52] And then also we're going to disable this field while our fetcher is inflight. So go ahead and say,
disabled is equal to, and say
todoFetcher.state !== "idle". So we're going to just disable this field if, for whatever reason, our state is anything but idle. So the way that the fetchers… The fetchers all have their own state. Again, we get all that from the server. It starts idle, then you're going to go into a different state as the request is happening, but then once it's returned, it's going to be idle again.
[01:16:30] Ben: Okay.
[01:16:30] Chance: So if it's idle, we can edit the field. We can only edit the field if it's idle.
[01:16:36] And then inside of this, we're going to render a label, but call it uppercase
<Label>, which we'll also import from our
ui/ directory. And this is just going to associate the label with our field. And call it "New todo."
[01:16:50] Ben: Like… oh, just in here?
[01:16:52] Chance: Yeah, it's just a… no, I'm sorry, just a string that says — It's just a static label for our field, yeah.
[01:17:02] And then below that we can render a
Field component. It's just called
Field. And also we're going to import that from
ui/ as well. And this is going to have a
value prop, and this is going to hold our value state
[01:17:19] Ben: And
value… yep. That's our state up here. Okay, okay.
[01:17:23] Chance: Yep. And then I think you accidentally imported
value from somewhere. And VS Code does this to me all the time, but I'm not sure.
[01:17:32] Ben: Just did a few Command-Z's there, so hopefully it's undone now. Okay, okay.
[01:17:36] Chance: Cool. Give an
onChange prop too, so we can set the value. And this part's not explicitly necessary,but, like I said, I want to clear the field after we deal with the data. So the
onChange prop's is going to have an
[01:17:51] Ben: Oh, okay.
[01:17:54] Chance: And then we're going to set the value to
[01:17:59] Ben: Oh, yeah, okay. Got it.
[01:18:06] Chance: Chat's getting quiet. Either everybody's completely lost or I'm boring the absolute crap out of everybody. I hope it's not that one.
[01:18:14] Ben: Let me liven things up in the chat.
[01:18:17] Chance: Hey, chat! Y'all alive? Come on, chat!
[01:18:18] Ben: Y'all good? Y'all good?
[01:18:19] Chance: "Zoned in."
[01:18:20] Ben: "Zoned in!"
[01:18:21] Chance: I forgot, option three is just completely zoned in and, like, mindblown.
[01:18:25] Ben: Alright!
[01:18:26] Chance: There we go. Just making sure we got some life out there. Thanks, y'all.
[01:18:28] Ben: We've got some light hypnosis going on!
[01:18:30] Chance: I'm very self-conscious. I need validation. I'm like a form field!
[01:18:33] Ben: Well, looks like there's some of that, yeah! It's somewhere in here. It's, like, in your utils.
validate, right there.
[01:18:39] Chance: That's right. Yeah, cool. So we got our
onChange. I think we're good on the field. So right below the
[01:18:47] Ben: Oh.
[01:18:47] Chance: After the
[01:18:49] Ben: Got it. Is this…? Can
<Field> be self-closing?
[01:18:53] Chance: Yeah, that's fine.
[01:18:55] Ben: Cool.
[01:18:56] Chance: There's no children. It's just an input. This is just the fancy UI wrapper that also provides all of the field data to both the field and the label. It's just easier, I think, than matching up IDs and all that stuff, and it's for ARIA purposes.
[01:19:09] So yeah, right below
</FieldProvider>, go ahead and render a
<div>. And inside the
<div>, just going to render an uppercase
<Button> component. This is a custom
<Button> component that we're also going to get from our
ui/. Yep, there you go! That's the one! Go ahead and give it a… don't worry about the classname. Just say "Create Todo."
[01:19:35] Real quick:
<button> has a
type attribute, right?
[01:19:38] Ben: Yeah!
[01:19:38] Chance: HTML
<button> has a
type attribute? What's its default value, without looking?
[01:19:42] Ben: The default is just
"button", but then…
[01:19:45] Chance: No!
[01:19:46] Ben: No?
[01:19:46] Chance: No!
[01:19:46] Ben: Is it
[01:19:47] Chance: I thought that, too! I thought that, too!
[01:19:49] Ben: Is it
[01:19:50] Chance: Yes, it's
[01:19:51] Ben: Really!
[01:19:52] Chance: I thought that, too. Remember when I say Remix has to reteach you HTML. I completely forgot that.
[01:19:57] Ben: Huh!
[01:19:58] Chance: I knew it at one point. I forgot it. But yeah, so the default type of a button is actually
"submit", so we don't actually have to say this is a submit button, but we want it to be a submit button.
[01:20:07] Ben: Interesting!
[01:20:08] Chance: Yeah, so you can say — Why is it…? Oh, it's just slow to catch up. But yeah, no. So this will totally goof you up in Rem— well, it'll goof you up in HTML period but, like, especially, like, in React… you know, how do we normally submit a form if we're writing client-side React code? We always prevent default behavior anyway and do our own client-side submission logic, right?
[01:20:28] Ben: Gotcha!
[01:20:28] Chance: So we don't have to worry about silly things like this. We just say "onClick, yada, yada, yada, yada, whatever."
[01:20:33] Ben: Okay!
[01:20:33] Chance: But when we're dealing with plain ol' HTML forms, we have to remember these types of things. So, if you have more than one button anywhere inside of a form, make sure you say
type="button", because its default value is
"submit". So, if it's doing anything other than submitting or resetting the form, it needs to have a
[01:20:49] Ben: And here, we are looking to submit. Like, that's what we want.
[01:20:52] Chance: That's right. I just wanted to call that out because, like I said, I totally forgot this at one point and had to relearn it. And I say, I think so many people either forget or just think that the default
[01:21:04] Ben: Yeah! Interesting! And we've got Michael Chan on the chat who says, "That's why you have to add the attribute. Because it can accidentally submit forms in older browsers." Wild — today I learned!
[01:21:13] Chance: Yep, yep! There you go!
[01:21:15] So, I think we're good here. So, this is our component. Like, we've written this
NewTodoForm component. Let's go ahead and render it in a couple of places.
[01:21:23] Ben: Okay.
[01:21:24] Chance: Go up to line… I think… I want to say it's going to be, like, 223 or something. There you go. So right after that separator, that
[01:21:31] Ben: Okay, right here.
[01:21:33] Chance: So right after that, go ahead and render our
[01:21:37] Ben: And this needs the list ID.
[01:21:39] Chance: Yeah, and it's going to just be… you're gonna get that from your
todoList object, so
[01:21:47] Chance: And just go ahead and copy that guy, the whole… the component you just rendered. And we're going to render it in that fallback clause if you don't have any todos as well. So right under where it says "No todos for this list yet." Yeah, there you go.
[01:22:02] Ben: Okay!
[01:22:02] Chance: Now let's save that guy, and then check out our UI.
[01:22:08] Ben: And it's already there!
[01:22:09] Marco says, "That's why every Button abstraction I built used to have type="button" as a default. Now with Remix, I think it kind of doesn't make sense anymore." Yeah, it is… I dunno, it's weird because, Marco, like, you ARE overriding browser defaults there, but also you're overriding them to a new default that I think is what most people expect. Eh - still trying to figure out how I feel about that!
[01:22:34] Chance: I used to do the same thing! Yeah, I used to do the same thing. I don't anymore mostly just as I'm just so used to being the default that I wanna… I think it's more for, like, my own memory. Like, I just… it helps me remember that that's the default, because if you goof it up, you'd be like, "Oh, yeah, that's wrong." So, there's also the type
"reset" too which is using forms to clear all the data.
[01:22:55] So, let's go ahead and create a new todo to test this guy out. So you'd make something else to do.
[01:23:05] Ben: Ooooh!
[01:23:05] Chance: Boom, there it goes! Pops right up on our incomplete todos.
[01:23:08] Ben: Ooooh.
[01:23:10] Chance: How did that work?
[01:23:13] Ben: Hang on.
[01:23:15] Chance: Let's add a little bit more functionality. Let's go back to our
[01:23:19] Ben: Okay?
[01:23:19] Chance: Because I mentioned clearing the field value after we submit.
[01:23:22] Ben: Yeah!
[01:23:24] Chance: So create — right under your
submissionAction variable that we didn't end up using, let's go ahead and write a
[01:23:33] Ben: It would help if I could spell "React" correctly, turns out!
[01:23:39] Chance: I don't spell anything right. I just rely on TypeScript to fix it for me usually. Nine out of ten times, it does, so.
[01:23:48] So inside of our effect, we're going to call a function — or this is a function, obviously. So, we're going to check if our
submissionAction… so check if
submissionAction. — so
submissionAction is going to be a string if it exists, so
[01:24:05] Ben: Okay.
[01:24:05] Chance: We're going to check that it starts with a certain substring. And since TypeScript's very smart, it knows that this might be undefined, so it does the optional chaining for us, which is very, very nice.
[01:24:15] Ben: Niiiice.
[01:24:15] Chance: And I'm going to convince you to switch to TypeScript on this, either today or later.
[01:24:20] Ben: I am sold on the value proposition of TypeScript. I just have not been… like, I've not, like, played with it enough. I haven't actually really started doing it. My new role though — it's funny, like, folks mentioned, like, oh, I'm contractually obligated to use TypeScript now.
[01:24:34] Chance: Yeah.
[01:24:34] Ben: Like, of course we use TypeScript at Microsoft. But, like, I'm sold on the value proposition just haven't actually really gotten to using it day in, day out.
[01:24:47] Chance: Cool, so now we've—
[01:24:49] Ben: Martin is calling out, "That todo app is starting to look like Basecamp."
[01:24:52] Chance: Shh. We don't… we don't say these things out loud.
[01:24:56] Ben: We don't say these things out loud!
[01:24:57] Chance: I'm just kidding. Total coincidence. Absolutely complete coincidence that we called it PM Camp
[01:25:03] Ben: Of course!
[01:25:04] Chance: Yeah. So, I never said I was that creative, okay? Let's get that out of the way right now.
[01:25:10] So, yeah, we've got our stuff. Oh, yeah. So what are we doing? We want to clear this field. So check that our action starts with… 'cause we remember, we might have… yeah, let's just go ahead and check this. We probably don't even actually need to check this, but I'm going to check it anyway 'cause I might want to create another action later. So check that it starts with
"/dashboards" — the same action that we used in our form. We can even say equal to, if we wanted to.
[01:25:37] Ben: Yeah, in our form, so…
[01:25:39] Chance: I usually say
startsWith just because sometimes we'll have params at the end or something.
[01:25:43] Ben: Ohhh, okay.
[01:25:44] Chance: But yeah, I'll just copy that action and pop that in there.
[01:25:47] Ben: I know we've got a few people in the chat who are, like, regularly using Remix. I would be incredibly curious, like, how y'all are going about this, because it also feels like you could use, like, a regex match and stuff like that. I'd be curious to know what y'all's preferences are here and if the Remix community has landed on conventions here.
[01:26:08] Chance: Not really. This is how I write it.
[01:26:11] Ben: Okay.
[01:26:13] Ben: Yeah.
[01:26:13] Chance: Like Remix isn't really forcing your hand on most of this stuff. So we might provide higher level APIs for some of these in the future. I think the goal for this first version was to get as close to all the platform as we could.
[01:26:29] Ben: Yeah!
[01:26:29] Chance: Like, we didn't want to abstract things too early. We didn't want to give you tools that you couldn't actually dig into and use for whatever UI you wanted to render. So we may explore higher level attractions than what we have today. In fact, I would bet money on it. Like, I think by v2, v3, you're going to start seeing some, some more conventions pop up—
[01:26:47] Ben: Okay!
[01:26:48] Chance: —to handle some of these things with a little less typing, but for now, they're pretty low level and that's by design.
[01:26:53] So, and someone else also said this, too: "I thought the goal of Remix was not to use
useState?" That is not necessarily the goal of Remix. The goal of Remix is not to get rid of React. We love React, right? Like, these hooks are core, fundamental pieces of React. I think of
useEffect in a Remix app as tools to provide progressive enhancement. Which is exactly what we're doing now, right? Right now, all we're this effect for… So, we're going to check to make sure if we're currently submitting, we want to clear the value in that form field, because if it's submitting, we're optimistically updating the UI while it submits, so let's go ahead and clear the value! Because if we clear the value, it's as if we've already created the todo. That todo is already inflight and it's handled up above in our other component in that useFetchers hook, right?
[01:27:43] Ben: Yeah.
[01:27:43] Chance: 'Cause It's going to be one of those fetchers that this thing catches. So it's going to pop up there immediately, it's going to clear the field immediately, and it's going to look like it just happens, right?
[01:27:53] Ben: Yeah!
[01:27:53] Chance: So this is a progressive enhancement right here.
[01:28:23] Chance: Oh, totally.
[01:28:23] Ben: And that's kind of the goal of progressive enhancement here.
[01:28:26] Chance: Yeah, we're not out to get rid of state or
useEffect or any of the built-in hooks. We're out there… We can help you get rid of a lot of them, because a lot of the things that you used to need state for and used to use effects for, you don't need to anymore.
[01:28:36] Ben: Mhmm.
[01:28:37] Chance: But that's not our goal. And we, you know, we really love React, and we love its core APIs, and we want to use them to create great experiences,so.
[01:28:44] So, we also want a dependency in this dependency array, too. Go ahead and pop
submissionAction in there, too.
[01:28:51] Ben: You got it!
[01:28:52] Chance: So, anytime our submission actually changes, we recheck and see if it's inflight. If it is, clear that value.
[01:29:00] Ben: Okay!
[01:29:00] Chance: And I think you can give it a save! Let's try it out!
[01:29:06] And if you fill out the… if you just add and press enter, now guess what? You should be able to keep on typing.
[01:29:15] Ben: Nice!
[01:29:15] Chance: It does lose focus, doesn't it? Yeah, we'll fix that. I'm going to make my own todo in PM Camp to fix focus management on this page.
[01:29:27] Ben: Alright!
[01:29:30] Chance: I think we have a default focus manager higher up in the tree that moves focused to the top—
[01:29:38] Ben: Okay.
[01:29:39] Chance: —to one of the other elements. Usually when you route change in a client-navigated app like this, you want to move focus to somewhere higher in the tree…
[01:29:50] Ben: Yeahhh.
[01:29:51] Chance: …to something interactive at the top of the tree, right? In this case, we don't want to do that. So I'm going to…
[01:29:56] Ben: "focus management, Marcy Sutton…"
[01:30:00] Chance: Yeah, so this technique actually came from Marcy Sutton. I'm glad you're searching for this because we don't build this into Remix like Gatsby does. And the reason for that is there are certain cases like this one that we're seeing now where we need to override this behavior—
[01:30:16] Ben: Yeah.
[01:30:16] Chance: —and we don't have good APIs for that just yet. So we're working on APIs, some abstractions internally to improve this experience as best we can.
[01:30:24] Ben: Mhmm.
[01:30:24] Chance: For now, we sort of leave it up to the developer to make these decisions. So, we don't want to abstract things that are really hard to get out of.
[01:30:31] Ben: Yeah!
[01:30:31] Chance: And that's the ultimate goal. But we do want to provide easier APIs for focus management overall, but there are certain cases where you really do need to take more granular control of this experience, and this is definitely one of those, so.
[01:30:42] Ben: I've definitely had experiences with messy focus management in React applications, and it's one of those things of, like…
[01:30:50] Chance: It's hard!
[01:30:50] Ben: It's so hard to come up with the right abstraction for because you could have a best practice, but the best practice is going to work eight times out of ten, right?
[01:30:58] Chance: Totally.
[01:30:58] Ben: Not even nine times out of ten, right?
[01:31:00] Chance: Yeah.
[01:31:00] Ben: And you need the escape hatch, and the escape hatch has to be just as intuitive because otherwise, like, you're just going to lead to a bad keyboard experience if your focus management starts fighting with itself.
[01:31:12] Chance: Yeah, for sure. And that article by Marcy is great, and that team did a lot of really great research. Because we really hadn't explored the multiple — like, the client-side routing community, like, we had Reach Router, which is something that one of my bosses Ryan helped build, right? But Reach Router was really sort of an experiment because we didn't really have best practices for routing in React and focus management. Like, we were figuring that out as well.
[01:31:41] Ben: Mhmm.
[01:31:42] Chance: And the experiences that worked really great for server-rendered or server-routed applications are not always the same for client-routing applications.
[01:31:50] Ben: Yeah.
[01:31:50] Chance: So there's still a lot to be explored in this area. The core web platform — like, the Chrome team I know is working on the App History API, and they're working on other APIs that might make this even easier in the future!
[01:32:01] Ben: Mhmm.
[01:32:01] Chance: There's been a lot of work done here, but there's still more to be done, there's still more exploration, and it's still sort of up to the developer at the end of the day to know how to handle those route changes and how to move focus once the routes change. And sometimes, you might not want to do anything when you change! So yeah, at the end of the day, test your application with screenreaders and make sure it makes sense.
[01:32:23] Ben: Yeah. Okay!
[01:32:27] Chance: But I think we're good! Like, I feel pretty good about this, aside from the sort of clunky CSS. We can make it prettier, but we can do that another time.
[01:32:34] Ben: For sure.
[01:32:35] Chance: I feel good about what we built today.
[01:32:36] Ben: Absolutely! So I think this is a great time to open the floor for questions from the chat. If there's anything you wanted to see more of or anything that you're not quite clear on, please feel free to drop your questions in the chat. But we'll be, I think, wrapping up fairly soon, so if you've got questions, get them in now! And yeah!
[01:32:59] Chance: Ben, did you have any questions for me?
[01:33:01] Ben: I… don't think so at the moment. Like, I feel like I would want to… Oh, actually! So, you know, at some point I would probably want to start, like, a Remix project from the ground up, right? Lik,e there was a lot here that was already built out, a lot of abstractions that were already provided — things that we didn't look into today. Like, we've seen a lot of the poor concepts of Remix, but this wouldn't necessarily be a great, like, starting point for someone who's not really seen Remix before, right? Like, so, for someone who might be looking to just fire up a new Remix project and kind of get started, like, from the ground up, like, what are some resources we've got on that front?
[01:33:44] Chance: Yeah, so all that information is in our docs. I'll tell you right now, if you pop open your terminal, we can start one up in about 30 seconds.
[01:33:51] Ben: Okay. Let's try it.
[01:33:53] Chance: Back out into your parent directory here somewhere. And then, run
npx create-remix@latest. At symbol, latest. Yeah, there you go. And then run this guy.
[01:34:07] Ben: Will this ask me for a name of a directory, or…?
[01:34:09] Chance: It's going to ask you some questions, yeah. So go ahead and run that guy. It's going to ask you… First you need to install the package, obviously
[01:34:19] I said 30 seconds. I didn't account for the fact that you need to install it first.
[01:34:22] Ben: Alright.
[01:34:23] Chance: So now, it's going to ask you where do you want to create your app. And it's going to default to
my-remix-app, but you can call it whatever you want.
[01:34:33] And the next question is going to ask, like, "Where do you want to deploy this thing?" And this is a question that I think will trip some folks up early on, because they're not… maybe you want to create an app and you're not entirely sure. You can just use our Remix app server if you're not sure, and it creates a plain old Express server for you in your directory.
[01:34:55] Ben: Okay!
[01:34:56] Chance: And once this is done installing, you'll just
cd into your Remix app, run
npm run dev, and you're ready to go.
[01:35:01] Ben: Cool. And I probably should not have actually told that to run
npm install, because I'm not really gonna look at that.
[01:35:07] Can we quickly real, like real quickly dive into maybe deployment?
[01:35:12] Chance: Uh, sure! S, we don't have our own hosting platform just yet. And I say "just yet." We may never have. We're not a hosting company. So, Remix's goal is to allow you to deploy pretty much anywhere. And we're going to have — as you saw in those initial questions, we're gonna have multiple deployment targets.
[01:35:32] Ben: Okay.
[01:35:32] Chance: So where are you actually deploy is totally dependent on your deployment targets. So it's actually — I think it's a good idea to sort of think about where you want to deploy and what that deployment looks like before you create a new app. If you're just playing around with it and you want to explore, totally start with the Remix app server, and you can even deploy that to any Node server you want. But we're not gonna — So we've got a README file in the root of this new directory that you just created that's going to walk through both the development and the deployment for this application.
[01:35:59] Ben: Okay.
[01:36:00] Chance: For the plain Remix server, it's just a Node server. So if you wanted to… you'd have to write your own script if you wanted some deploy-from-GitHub situation. But we also deploy to Vercel. We have an adapter to deploy to Vercel. We have an adapter to deploy to Netlify. So if you're familiar and comfortable with those platforms, you can deploy there, and the README file will walk you through your GitHub synchronization and deployment techniques for those targets.
[01:36:27] Ben: Okay.
[01:36:28] Chance: But it is going to look a little different depending on where you deploy.
[01:36:31] Ben: Did we go with a Remix app server for PM Camp?
[01:36:37] Chance: I believe so, yeah. Yeah, I believe so.
[01:36:40] Ben: Okay, cool.
[01:36:41] Chance: Because I'm ultimately just running a local server.
[01:36:44] Ben: Got it. Okay, cool.
[01:36:46] I think… It doesn't seem like there's been any questions from the chat, so I think, let's go ahead and start wrapping this up! What do you think, Chance?
[01:36:55] Chance: I feel good! Yeah, like I said, this code is going to be up here pretty soon, and I'm gonna share the link so everyone who watched will have access to dig in even further! But in the meantime, check out my Twitter. Check out — oh, can we plug my course?
[01:37:07] Ben: Let's do it!
[01:37:32] Ben: Yeah!
[01:37:33] Chance: —since I can do that, too. I forgot I can do that. Give me just a minute. I gotta get that link.
[01:37:41] Ben: In the meantime, I'm going to embiggen us, and we had a question earlier, just someone who wanted to see my excellent sweater again. So here it is in in big, as I arrange myself in the camera there. Maybe I'll get, like, extra close. So yeah, big ol' T-rex sweater. T-rex is wearing the same sweater. You gotta. Okay.
[01:38:08] Chance: Recursive! I love your sweater, I really do. I think of myself with my Santa hat as the young Santa from "Santa Claus is Coming to Town"—
[01:38:18] Ben: Okay.
[01:38:18] Chance: —that claymation Santa movie, 'cause he's got a big red beard! And mine's a little smaller now.
[01:38:22] Ben: Mhmm.
[01:38:22] Chance: But he's a red-bearded Santa in his youth. So, think of me as young Santa, even though I'm not that young. Getting kinda old. But anyway. I plugged my course.
[01:38:32] Ben: Yes!
[01:38:45] I don't know. That's all I got. Check out the Remix docs. I think Remix is great. If you have any questions, I'm around on Discord, Twitter, anywhere.
[01:38:54] Ben: Remix has a Discord, right?
[01:38:56] Chance: We do! Yeah. If you go to remix.run, there are links to the Remix Discord, if you want an invite there. It's all on the public. I think it's under Resources or something. I don't remember. It's somewhere.
[01:39:10] Ben: It's probably on the docs and not the homepage.
[01:39:14] Chance: We should probably fix that.
[01:39:16] Ben: Yeah, okay. Let's see if we can find… There we go.
[01:39:20] Chance: It's under Community. There you go. Boom. Yeah, I'm going to make a todo. I'm gonna add that to the actual navigation.
[01:39:26] Ben: So, Marcos is asking if we would talk about Reach UI. We actually sort of have already together! That was when we did the tab component, right?
[01:39:37] Chance: Yeah, we did that in another stream.
[01:39:38] Ben: Yeah.
[01:39:38] Chance: So if you've got a link to that video, that would be a really good one to dive in to Reach UI.
[01:39:42] For those that don't know Reach UI is a UI library. Ryan Florence initially created it. I started working on it and started maintaining it a couple of years ago. I don't maintain it as much anymore just because time. Like, I'm a busy fella.
[01:39:58] Ben: Mhmm.
[01:39:59] Chance: But it's a pretty good community that has been using it for a long time and most of the components are fairly stable at this point, so definitely if you're interested in that sort of thing, check out Reach UI! Yeah, and if you have any issues with it, like I said, I'm around.
[01:40:12] Ben: Ah, the "future of." I imagine you might tweet about that, perhaps, so follow — I'm not going to ask fo future-of stuff right now on stream, but reach out to Chance. Yeah.
[01:40:28] Cool! Let's go ahead and call this done! So y'all, while typically we stream every Tuesday, I'm not going to be streaming next week because holiday reasons. So I will see you in the new year for January 4. I actually just had to reschedule my January 4 stream, so I'm going to figure out something good to do. But if you enjoy things like accessibility and core web technologies, if you enjoy seeing guests such as Chance here, y'all should hit follow! You should also go to twitter.com/SomeAnticsDev where I'll be tweeting all the updates for all the upcoming streams.
[01:41:08] But yeah, with that, I will see you in the new year. Thank you all so much for being here and joining me on this journey with Chance!
[01:41:16] Chance: Ben, it's been great! Come hang out with us in Discord. Chat with me and all my really coworkers, and we'll see you on the internet.
[01:41:23] Ben: Awesome.
[01:41:24] Chance: Thanks, Ben!
[01:41:24] Ben: See you around. Bye, y'all!