Transcript
This transcript was autogenerated. To make changes, submit a PR.
Hi, my name is Ken Snyder and I want to share my journey of
using Remix in my web app application and why you
might love it as much has I do. I'll cover three things in
this presentation. First, why I chose it for my project.
Second, I'll introduce you to remix by creating
a small app. And finally I'll talk more about remix philosophies
and approaches and how they can be applied outside remix let
me introduce myself. My name is Ken Snyder. I founded
the company Shoreline, which offers a web application
that allows cancer doctors to provide patients with
specific and well timed educational resources throughout the
treatment process. I am a co founder of UtahJs,
a group for JavaScript engineers to get together for lectures
and networking. And this presentation is on Remix, which is
a meta framework, meaning that it doesn't replace react
or code, but sits on top of both of them and really
not just node but any javascript or typescript runtime
for us. We are in the situation where patients are
viewing education information on slow cellular networks.
They're looking at only one or two pages and we want
them to see the content as soon as possible and interact with
it as soon as possible. Remix comes from the minds behind
react router, which is the gold standard for building a react
app, and remix is actually a company.
It's staffed by full time engineers. Earlier this year
they were acquired by Shopify, and so they have a lot of attention and resources
driving this framework. So let's go ahead and dive in
and create a remix app. A really easy way to get
started with Remix is to use MPX create Remix. You give it
a directory and you can also pass it a template and
it will create a new application for you using that template. There's over 100
templates in the community and this case we're just using the basic
default template, but we can go ahead and open
vs code to that folder and
as we go in we can run NPX,
run dev, and that will create a server for us.
It will add a hot module reloading and as we visit
this new application we get a welcome screen.
This is defined in the routes folder as
underscore index that represents the slash route
or the root route. And in here we're
going to go ahead and create a new page and it's
going to be shop and so we create
a file inside of the routes folder called Shop TSX.
With that all we have to do is define a component and
whatever's in that component will get rendered at that URL. In this case
we're just creating a header so that we can see and demonstrate
that we've got this route working.
And then the second thing we're going to do is we want to create
a page inside of this shell. So down
below the header we're going to create the list of shopping items
and allow us to add things to the list and to
put things in our cart and to delete them and to
just have this shopping cart kind of application.
So we're creating this outlet
and inside the outlet we want to render a child
page which is shop items TSX.
So we put that in the routes folder. And now whatever
we return here from this component will be
put and rendered at shopitems.
So we'll go ahead and create just a hello world page so
we can see that it's working. And then
let's go ahead and load some
items in. So we don't have a database but we're going to go ahead and
just load items from memory. Here we're
going to use this function called JSon. It's a remix function that
just creates a response object with
content type JSon. And this case we're passing
it in one piece of data, which is items. And instead of
items getting from the database like we might normally here,
we're just going to define some in memory here that will sit
with the server. The id is just the
new date. So normally that would be an id from the database, the name of
the item, and then we're going to say that it's not in the cart yet,
we've just put it on our list. And these need
to code into our items component. And the way we pull them in is by
using this function, use loader data. So one thing
that's nice about use loader data is that you can use
type of loader to tell typescript what data is going to
come in. So we know that
the items have the shape defined in line
four and those items come out of
the loader function using the JSON function.
And so when we tell typescript to
use the type of loader, then in our items component here
we actually get a list of items and we know exactly what types
they are. And so we get that nice transition of types between
server and client without having to do any extra steps
or define any schemas
or sharing and defining
lots of types. So we're just going to go ahead and print out
the items using a pre so that we can see them come in.
And then we're going to make a way to create an item.
And with that all we're going to do is create a form.
The form is going to have one thing which is an input for
the name and then a submit button. So we can go ahead and see
in here. If we add something and click add item, we've got
a 405 because we don't have any way
to handle that post yet. The way to handle that post is just an export
function action. And the action basically will
respond to any posts or other verbs,
other methods. And in this case we are reading the
form data object. So because it's a form post,
it's sending a form serialized in the normal
browser, HTML form serialization, and we can
pull that data out has a form data object by awaiting
request form data. We're going
to pull out the name from the form data which is the name of the
item. And we can do that by just calling get name
and we should have a string representing the name
of the item. And we don't have a
database here. But again, we're just going to add the new item with
an id, a name and represent that it's not
in the cart neither. So as we do that and we refresh,
we can see, oh shoot, it still doesn't work.
What do we need to do for the action? Well, it says you have to
return something from the action. So in this case we don't
really need any success messages or additional data
sent back to the user. We just return null. And whenever
we do this remix is automatically calling
the loader again to get the new data. And so if we type
in a new item and click add item, the way it gets added
is that it's going to go into the action,
push the item and then it'll go into the loader and load
the item and rerender the component with the new items.
You'll see here. One of the problems is that the
text stays in the text box. We'd love it
to clear out that text so we can add an item over and
over again without doing anything special. A really
simple way to do this is to force react to rerender the form
anytime the items or the number of items changes.
And so that will actually clear out our form anytime that
we type something in. One thing though, if we
type in and press enter, we're not going to be in the
input anymore. So all we have to do is do auto focus.
And now the browser will automatically keep focus into
that input as we add more items.
So let's go ahead and actually take the
items and display them in a reasonable way.
We're going to use an unordered list and each of the items will be
a list item. Here we're using the id
has something unique. Again, this might be an id from database
and then we're just going to print out the item name. So you
can see here we've got a nice unordered list with our items on
it and now we want to be
able to delete items off of our list. And so
one way we can do this is do a post
to shopitems and the item id.
And we're going to go ahead and display it inline just for this demo so
we can have the button next to each item.
And you can see we're doing something special with this button.
It's not a normal submit button. It has a name and a
value. It turns out in javascript and in HTML
you can just give a button a name and a value and when you
click on that button it's as if that form contained a
hidden input with that name and value.
So this will allow us to tell that the delete button
was clicked. When the form gets serialized it will have one value with the
name of operation and a value of delete. So we need to go ahead and
handle that form. And so we're going to create a new file in shop
items dollarid. What that does is create
a new URL for shopitem
and then the id of the item which will allow us to mutate that
particular item. The dollar id represents the name
that the param that will come in and so
we can just read the params inside the action.
So we'll read the iD, we'll parse it as an integer
and then we'll take the form data from that
form and deserialize it. So we just have the one
thing that we're interested in, which is the operation, and we're
going to handle when the operation is equal to delete
and we're going to just act directly on that list of items
that we defined in our other route. Normally you
would be doing some sort of database manipulation here, but anyway,
what we're going to do to delete item is just find
the index of the item with that particular id. And if it's found we're
going to splice out the items at the
given index. And as we learned before, with actions you've got
to return something. So here we're going to return null.
But now you can see if we type in a new option,
we can add it and then we can also delete it. So let's
add the ability to mark it as loaded into your cart.
We're going to use another button. We don't have any more hidden inputs.
We're just adding a new button with a name and an operation
and the value of load. And that allows us to create
a second mutation which is to load it into the cart.
And we're going to represent loaded items by just giving them a
line through text decoration so we can
see the item is still there and that we've purchased it or that we've
got it in the cart but it's not deleted from the
list yet. And so here we're going to handle
that load operation. And before we do
that, I've realized we can refactor a little bit here.
We're always going to be operating on one item. So let's find the item first.
And then if we delete, if we need to delete the item, we splice it.
If we need to load it, all we're doing is saying that it
is in cart of true.
And so as you can see, we can add an item and we can load
it in the cart and it will have that nice strike through
on it. Last thing is be able to unload it from
our cart. So we're going to create a new button that does unloading.
And again, it's going to come in here to the same action and
we're going to handle the case when operation is
unload. And it's very similar. All we're doing is marking as in
cart as false. So now we can add items,
we can delete them, we can load them in our cart. We can unload them
in our cart. And now we've got a very simple and nice little
app with just very few lines of code.
If you're interested in doing more with this particular code,
you can try it on GitHub. I have it at this URL and
this QR code. One more thing I kind of
want to mention about outlets. So outlets can
be representing kind of your series of URLs.
So in this particular app here, we have
some general information like the header
that comes across the top and that can be represented at the root
route. And then when we load
projects, we're loading this green area, which is
a list of projects. And if you click on a project,
then you'll get to the slash 1234 part of the URL
and that will render inside of the green which is
represented by red. So it allows you to really organize your
code really simply when it comes to parent child relationships.
And of course you don't have to do that, but it's a nice feature for
those kinds of situations. You might say
the forms that we dealt with were really simple and
let's talk about something more complex. So here
is a user's table. How in the heck would you represent
this with just form actions?
Normally your
first thought would be okay, we're going to add a click handler to each of
the pages and we're going to add a click handler to the sorting
and we're going to capture the submit
and apply the filters. So let's take this and
we'll say that the form is this box right
here and that these are actually all buttons.
So we don't have any on click handlers, we just have an outer form
and inside the form we have all of the submit buttons.
So the filter will be just a regular submit button.
So if you type something into search, you click filter
and then it's going to go ahead
and post that and you can do
a search. And then we're going to use our trick
that we learned from before, which is that buttons can have names and values.
So the sort buttons, for example,
this one is the email address. To sort
upwards or ascending, you would send the value
for sort as email and you could
represent the descending with email or email
descending or however you like to do it.
Then the pages, they represent a page
and a value. And so for example, if we were to make
this a get form, you could see in the URL that we
would have the name page equals
two and sort equals email
and filter equals bob. And that would
all be in the URL. It would work right
out of the box where it would search immediately if you went to that URL.
Or it would just update the URL as you're going along clicking on
these buttons. Let's talk about the remix philosophy.
So as we saw, the way to write server and
client code is very simple. We didn't
think a lot about what runs on the client and what runs on the
server. We are working with web foundations. The requests
that come through, for example, are regular request
objects and the responses are regular response objects.
And we'll also talk a little
bit later about how these kinds of applications
can work without javascript and why you might want it to work without javascript.
And we'll talk a little bit about the roadmap and
the future flags, concepts that they embrace.
So talking a little bit more about the server client model,
basically I'm kind of person that's interested
in how in the world does this get bundled. So we saw that there
were a loader, a component and an action.
And in doing that, how is
the bundle created for the browser, and how is the bundle
created for the server? So this is basically how it goes.
If we look back at our shopping cart application,
all of the things are in the server bundle. So everything
gets bundled on the server and that makes the server capable of
fetching data and writing and generating the HTML
and then the browser, all that it needs is the part that
generates HTML and the remix
runtime which will automatically handle form submission and
navigation and load things via
fetch and kind of comparing it
to a single page app. What you traditionally
would see is the HTML first comes down to
the browser and then the browser sees that it needs to download a bundle and
so it has to go back to the server to get the bundle. The bundle
will finally tell what data needs to be fetched and then
when that data is returned, it can generate the HTML.
Compare that to a remix application where on the first load
the server is able to grab the data, generate the HTML
and return that in the initial response. So immediately the user can
interact with the page. What will be at the bottom
is this client bundle, and its purpose is to hydrate
the page. And by hydrate we mean make additional
interactive things work. So with forms
it will automatically handle them and submit them via
fetch. And for links it will use
react router to load only the parts of
the page that change. So it adds efficiency. But it's
not absolutely necessary for the application to work.
And one way to maybe visualize this is to think about a waterfall.
With remix, the page is fully painted and you
can use it, and then by the time all
of the hydration occurs taken, it's fully interactive in terms of
anything that's special to JavaScript. So for example, if you've
got Tinymce, obviously remix
can't completely paint that from the server side
because it takes a
lot of client side JavaScript to make the
tiny MCE work. And you can compare that to
single page apps where it takes multiple
waterfalls until the page is even painted.
One really great feature of remix is that there
are lots of server and runtime adapters. So you can
use new runtimes such as Cloudflare workers, Dino and
Bun, and you can use services like
Netflix, Azure and AWS.
It supports all of those things. And also you can
have actually just more traditional
server frameworks, including exprs,
fastify and many more. And if you actually look at the
official list, there's 116 repositories
right now of stacks that people have created.
And these are various combinations of technologies. And it's
really great to see people from the community creating these
stacks and making remix work in so many different
ways. And I do want to just highlight the fact that
we've got some really great concepts in Dino and bun
that are coming out and code as it's
getting to version 20 and 18 and 20. There's call these
amazing new things, and I love that we can just pick Remix now and
it will work with all of these things going forward.
So the second thing to mention is that we want to
understand and work with the foundations of the
web. So you may be familiar with this episode,
this reference where Roy and Moss get together
and convince Jen that this little box is the
Internet. And so we have a bunch of great
standards. As I mentioned, requests and response are
things that remix does right out of the box.
We also have both server side and client side, the same
fetch API as of node 18.
And basically remix
uses that fetch mechanism internally.
We also saw how you can use form data to serialize forms,
and a few other things that we'll cover in a bit
here. Headers, how to read headers and set headers,
how to work with URLs to parse them or
to build them, and the parameters to serialize and
unserialize query parameters in a URL. And then remix
also takes advantage of some
prefetching and caching that's built intro the
web and built into browsers. And we don't know a ton about these,
probably because we're not used to making use
of them. But Remix has kind of done
the hard work for us and automatically
uses prefetching and caching in its mechanisms for
adding those features. And then we also talked about HTML
forms and how remix embraces those in order to
do any kinds of mutations.
So just to kind of give you an example, the way request
and response both work on server and on client,
we have these newest runtimes, Bundino Cloudflare
workers, for example, they all use a raw request
and they all expect you to return a new response.
So these are really interesting primitives.
And obviously these functions here that fetch and
receives a request, that's where you would add some sort of
adapter that does the routing based on the request URL.
And that's sort of how remix can plug into each
of these. And you may not have realized it,
but when you're using fetch, you're really using request and
response. So fetch and request have the
exact same signature. You can take a URL and then you
can also take an initialization where you can set the
method and the headers and the body of the request.
And so what you really can actually do is just create the
request and you can send the request to fetch.
So that's an alternate signature for using fetch.
And one way to think about it is that fetch
takes in a request and returns a promise with a response.
You can also use headers when the request comes through.
It has a headers property, and this is actually an headers object.
And headers has things like get and set.
Get allows you to get the header regardless
of uppercase or lowercase letters, because the
HTML standard headers are not case sensitive, so it's
easy to work with them. If you just use the get and set
methods, you can create a new headers object by
just passing in plain JavaScript object,
and the key value pairs will be used to create
the headers. If you need headers, for example,
that repeat. So if you have two cookies that you need to send,
then you have to have set cookie twice in
order to do that. You can't use a plain JavaScript object, but you
can send an array of arrays. So in this headers object we
have three headers. One is content type, one is set cookie,
and another is also set cookie with a different cookie.
In a similar way you can serialize and serialize
search parameters. With the URL search params constructor
you can pass in one of three things. The first is
just a regular string that you might see in the URL.
Second is a plain old JavaScript object
that will have the keys and values as
properties and values in the object. And in
a similar way you can handle when search params have more than one
of the same thing. So in this example maybe we
are doing a search form. We're filtering by the word hello,
we're sorting by name, and we're saying that we want
things that match either the tag JavaScript or
the tag typescript. You can use things like
fetch and form data in a traditional react component. So in
this case we have an add user page where
you can type in first last email of the user. We're going
to actually take the submission and prevent
the default submission from happening.
In the form data constructor you can pass in the
form element itself, and then you can deserialize
it by using object from entries, and that will get the form data
out into these variables first, last and email.
And then you can do more of a traditional kind of post to
a server or API where we're going to
post it with content type application JSON and we're
going to send that body JSon stringified with the three values that we have
here. Now what's interesting is that you
can actually do it without serializing into JSON.
So if your server is prepared, you can receive
the form data object as an object and just pass
it in that way and so you don't have to destructure the form
or serialize it into JSON.
And you may have noticed here that
that's what remix is doing as well. In our examples,
the form data we were getting from the request object,
and this is what it looks like on the clientside, sending that up.
Or you can just use remix. So the nice thing about remix here
is that we don't have any logic around submitting.
We can just have a form component and
it will automatically capture
the submit, use a fetch and navigate to the correct place
after that. As I mentioned before, there are some times that you may
want your application to work without javascript. So in
our case we've got patients coming to look at some pages
and we love this idea that the first render
they get all of the HTML, but also for forms
that are in there, they still work before hydration because they
act like just regular HTML forms and submit to regular
URLs. The other reason is we
ran intro a need for supporting IU eleven. So one of
the electronic medical records providers, very popular
one, they actually today are selling their desktop app
with integration ability only available through ie
eleven, kind of framed in thing inside the Windows app.
So we have a need for our application to work
with ie eleven. And if you look around at a
lot of the popular frameworks and everything that we've been
using, we're thinking we're going to have to check every one of our dependencies.
We're going to have to find out which
polyfills we need to use, which ones don't have polyfills,
which ones just can't work at all on old browsers. And that's
going to be really painful. So I first started out by going
to remix's website and saying, hey, do they have
support for ie eleven? And they're like, well, we use
script type module, so anything that's a little bit older
than three or four years ago won't even be able to load the javascript
because those browsers only know how to download
scripts that are script type javascript. So that worried me
a little bit. But then the following sentence was talking about how
you can actually make it work with Netscape or any
browser that's ever been made, because you're
just using regular forms and regular navigation.
And so let's take a look closer at
what our example application looks like without
JavaScript. So back to our applications. We can open the
root TSX file and you'll see in there that it has a reference
to this special component
called scripts. And if we take that out, if we
comment that out, basically what we're going to end up with is our
application will have no javascript and you can see there
in the network tab that we don't have any network requests
going on. So you can add items, call you want. What happens
if you go to delete the item? Oh no, we've got this returning
null. Well, what we should have done in our action is actually redirect.
So you kind of have to think back to fully
multi page app to how those actions should redirect.
But once we have those, it is reloading
the entire page each time. But this form situation
is working wonderfully for us to do whatever kinds of
additions or changes we need to just with regular
forms. And then the last thing that I'll mention here
about their philosophy is the way they do their roadmap and this
concept future flags. So what's nice is that you
can go to GitHub, you can see the rfcs for each feature
that are coming up or being proposed,
and you can comment on those and you can give your ideas
and you can add your own rfcs. And it's really great to have that level
of transparency and interest from the
creators of the framework. I'll just mention some of the highlights that
they have on their roadmap. By the time you're watching this,
I think they'll complete the Veet build system.
So they have been using a build system based on ES build,
which is what Veet uses, but they're actually changing
remix so that it's just a plugin within vite and that allows a lot more
flexibility and will add a little bit of speed.
They're also looking at loader and action middleware, which will allow you to
do authentication or other checks
before each and every action or loader.
And they're going to give some more tools for optimistic updates.
As you saw in our app, it was pretty naive and any
changes that happen rely on having a loader reload
the data when the action is done.
So real world APIs can
take a little while. And so it's nice to be able
to have some tools where you can update the UI before anything
actually comes back from the server. And I love this idea
of future flags that they've done. So remix recently came
into version two. Before that, in version
one, your remix combination file looked something
like this. There were six breaking changes between V
one and V two, and what happened was you could opt into
them one at a time. So for example,
error boundary syntax changed. And so
in your application you could say okay, I'm going to go in and I'm going
to edit every single error boundary function,
and then I'm going to put this future flag v two error boundary true,
and then the application will run normally and we've
already done one 6th of the migration,
and if you're really on top of it, you can do these migrations as
they come out, so that by the time
the new version comes out you're already ready for the new APIs
and the breaking changes aren't a big deal. And I love that because
it's so easy for you to use some framework
and then when a new version comes out it seems like so daunting to go
back and change everything and deal with all the breaking changes.
So this makes it very nice. It allows you to get new features
faster and allows you a lot of freedom in terms of when you
want to update these breaking changes.
So to recap the philosophy and how you might
use it in other contexts. So in any
application you can directly use fetch and form data
and URL, and you can learn those web standards and
they're not super hard and they're meant to be really
helpful and useful and eliminate
the need for a lot of NPM modules that
do the same thing. You can even learn
about runtimes like Cloudflare workers and
Dino and bun, because your request and
response objects are basically the same as what you would
work with in those runtimes. And then it
also is nice to think about what can we do to make the experience good
for people on mobile with low cellular
speeds, and what can we do for any
other kinds of challenges that you might run into,
like a user not having javascript at call.
And to recap what I love about
remix, when I write remix route, I don't have to worry
where my code runs. It could be server side, it could be
client side, it could be the first render, or it could be
that the user has JavaScript disabled.
So it's the same kind of code regardless
of that, and you don't really have to think about it. In fact,
for the first several days that I use remix, I turned the
script tags off completely just so I could
kind of see and understand how these form mutations work
and kind of the different mindset and how it's liberating.
Because of these kinds of patterns, I don't have to think about
duplicating code on the client or server,
I don't have to run into all of the gotchas and pitfalls
of server side rendering, and the performance
is just great. It's great for first renders, it's great for subsequent
renders, and I don't have to think about the different difference
between them. Finally, I'll just mention that if you're interested in
learning more about line and what we do for
cancer patients, our website
is at Shoreline Health. And if you
have been, thanks for watching, and thanks to comp 42 for the opportunity
to speak. Have a great day.