First thoughts on Remix
I gave Remix a quick spin following their “Jokes App” tutorial and gathered some notes. These are just my initial thoughts during my first contact with the framework. For a proper introduction, read the documentation.
The website is the result of a great amount of marketing effort. It’s amazing what JS frameworks do these days to get noticed. Regardless of whether the framework lives up to the hype or not, I get a strange feeling from the website: the language attempts to be humorous and light, and this doesn’t help me build trust.
From my notes, the most interesting parts are probably on the <ScrollRestoration />
component and on the loader
pattern.
On setting up Remix #
- Remix nuked my
README.md
file (on which I already had some notes) and I had to recreate it. Should have expected that! - Since Remix picks up on certain exports from files (like
export const meta
) but my IDE doesn’t know about that, I’m getting a bunch of “unused export” linter errors on those lines. I’m not sure where the errors are coming from, since from my recollectioneslint
doesn’t complain about unused exports. It has no knowledge of the other files. It’s probably IntelliJ, then. - The default
root.tsx
contains several interesting elements:<Outlet />
which is like a Svelte<slot />
: it renders children components.<ScrollRestoration />
, probably aptly-named. Its presence surprises me because this used to be a feature ofreact-router
that got dropped after the developers noticed scroll restoration working increasingly better out of the box on major browsers.TODO
: find out how<ScrollRestoration />
is implemented.- Remix documentation on scroll restoration mentions that this component works
by restoring the scroll level before rehydration. This should eliminate the jarring effect of
having the scroll point being restored after the page loads completely.
- The “before rehydration” part is true only by virtue of the
<ScrollRestoration />
component being used one line before the<Scripts />
component.
- The “before rehydration” part is true only by virtue of the
- The
yarn build
command finished in 0.44 seconds. Nice! - I like that the default app doesn’t contain images or marketing text selling Remix. It just has some documentation links.
The <ScrollRestoration />
component #
It’s implemented here and works roughly so:
- It disables the browser implementation with
window.history.scrollRestoration = "manual"
, but only in auseEffect
call that runs afteruseLayoutEffect
(used for other effects in the component). - Provides a
<script>
tag that runs a simpler effect (it’s simpler because it doesn’t need to know about browser history, since it runs only on the initial script load before hydration). - Only after the component has hydrated does the component run some more complex logic (e.g. to
respect
location.hash
) that ultimately may runscrollTo(0, position)
. - Scroll positions are stored in an in-memory
positions
dictionary where the keys arelocation.key
s. - To survive for longer,
positions
gets added tolocalStorage
and is used to restorepositions
into memory when the script runs again.
On first-contact DX #
- Live reload works great and spinning up the development server takes no time.
- While adding some code and missing an import, the app showed me a build error (expectedly). But then after fixing the error, the app didn’t go back to a normal state, and the error remained, even though the build didn’t show the error any longer.
- Adding the new routes, I’m thinking that the structure recommended by the tutorial
where
jokes/new
shares a namespace withjokes/$jokeId
is not great. What if I wanted slugs instead of IDs, and someone created a joke titlednew
? Other solutions aren’t as pretty, though:jokes/show/$jokeId + jokes/new
, ornew-joke + jokes/$jokeId
. - By exporting a
links
array, you can add<link>
elements to<head>
sort of like withreact-helmet
. They then get picked up by a top-level<Links />
component. - The tutorial recommends a file structure for styles that’s not great: a
styles
directory separate from other parts of the app, such asroutes
. If the CSS I write is encapsulated together with a route, then why shouldn’t their files live together? Having a separatestyles
directory adds quite a bit of indirection and could make a project hard to navigate.
On TypeScript use #
- The default
entry.server.tsx
file contains ahandleRequest
function that takes arequest
. That’s fine, but it also takes aresponseStatusCode
andresponseHeaders
. Am I still able to decide what status code to respond with? This signature is a bit weird to me.- Looks like there’s a default
200
response and I still have a chance to change it, as expected. Still, the signature feels awkward.
- Looks like there’s a default
- I wonder why the tutorial recommends using
export let
. It looks to me as if the things I’m exporting shouldn’t ever be reassigned. I’m changing these toexport const
, hoping that nothing explodes.- It looks like in other areas of their documentation they use
export const
, which makes more sense.
- It looks like in other areas of their documentation they use
- The
loader
pattern (see related docs) with anexport const loader
coupled with auseLoaderData
call imported fromremix
feels a bit weird to me. I suppose there must be quite a bit going on at hydration time for this to be worth the indirection. My first impression is that it shouldn’t be necessary? Why can’t I goexport const loader = someRemixUtility(async () => {})
instead, removing theuseLoaderData
call inside the component? The client and server bundles can still provide different implementations ofsomeRemixUtility
.TODO
: find out whyexport const loader
can’t be used directly and needs to be accessed viauseLoaderData
. My guess: the purpose is to call the loader during server rendering and then to reuse the same data during rehydration, to initialize a frontend cache with. Remix developers simply decided to go for a standard way to access Remix functionality, and this just looks consistent.- It looks like I was correct: Remix hands off serialized data to the client as a string and then has the client route reuse this data. It works similarly to what Apollo GraphQL recommends.
- There’s another insidious result of the indirection introduced by the
useLoaderData
andexport const loader
pattern, aggravated by the fact that the suggested typeLoaderFunction
isn’t generic: discrepancies between whatloader
actually returns and whatuseLoaderData
returns aren’t going to be caught unless a type is shared between them. ButLoaderFunction
isn’t generic, so there’s no enforcement from Remix to make sure that this is the case. I can have aloader
that returnsnumber
but then accessstring[]
inuseLoaderData
and unless I actively share the types, TypeScript won’t have a chance to complain. In my opinion,LoaderFunction
should take a required type argument for the return type, and another for query parameters, which can probably be optional.- One could argue that this is the responsibility of the developer, and it is, but by not requiring any type arguments, Remix isn’t helping.
- It’s also inconsistent because
LoaderFunction
takes no type arguments butuseLoaderData
takes a type argument for the returned data.
- When submitting data, the type mismatch is more accentuated, because
POST
requests containFormData
, which bears (as far as I know) no information on the shape of the data from the form. In this case, however, it matters less, because the tutorial calls the user to perform backend validation/parsing of this form data, by goingform.get("field-name")
and then validating the result.- I wonder whether building a way to type-check JSX forms and providing e.g. a TS
eslint
plug-in that uses the full power of the AST to build type-safe forms would be worth it. I suppose exporting a simple component for use,TypedForm<T>
would be enough, or perhaps even more magic, somehow simply type-check<form>
elements based on their current children. This is probably not possible because one would need to go into other modules. Maybe theTypedForm<T>
approach is good as long as one sharesT
with child controls.
- I wonder whether building a way to type-check JSX forms and providing e.g. a TS
That’s it for now! I’ll probably give Remix a proper try once I have a need for it. I feel like the typing issues I mentioned can be avoided through discipline, and the DX looks promising.
Relevant documentation:
- Remix documentation
- The
loader
pattern in Remix - React Router documentation on scroll restoration
useLayoutEffect
documentation