-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(vite): strict route exports #8171
Conversation
🦋 Changeset detectedLatest commit: db780a2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks for being so thorough here!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one! I've added a couple of docs suggestions, but this looks great.
1ab4e91
to
950d2c6
Compare
950d2c6
to
db780a2
Compare
🤖 Hello there, We just published version Thanks! |
🤖 Hello there, We just published version Thanks! |
From the added docs:
With Vite, Remix gets stricter about which exports are allowed from your route modules.
Previously, Remix allowed user-defined exports from routes.
The Remix compiler would then rely on treeshaking to remove any code only intended for use on the server from the client bundle.
In contrast, Vite processes each module in isolation during development, so cross-module treeshaking is not possible.
You should already be separating server-only code into
.server
files or directories, so treeshaking isn't needed in those cases.But routes are a special case since they intentionally blend client and server code.
Remix knows that exports like
loader
,action
,headers
, etc. are server-only, so it can safely remove them from the client bundle.But there's no way to know when looking at a single route module in isolation whether user-defined exports are server-only.
That's why Remix's Vite plugin is stricter about which exports are allowed from your route modules.
In fact, we'd rather not rely on treeshaking for correctness at all.
Treeshaking is designed as a pure optimization, so relying on it for correctness is brittle.
Not only that, but relying on treeshaking also means that if tomorrow you or your coworker accidentally imports something you thought was client safe, you might end up with server code in your client bundle.
In short, Vite made us eat our veggies, but turns out they were delicious!
So instead of treeshaking, its better to be explicit about what code is server only.
For route modules, that means only exporting Remix route exports.
For anything else, put it in a
.server
file or directory instead.As a simple mental model, think of a Remix route module like a function and the exports like named arguments to the function.
Just like how you shouldn't pass unexpected named arguments to a function, you shouldn't create unexpected exports from a route module.
The result is that Remix is simpler and more predictable.
Additionally, user-defined exports are a footgun for HMR as the Remix and React Fast Refresh may not have a way to handle updates for custom exports. See https://remix.run/docs/en/main/discussion/hot-module-replacement#supported-exports .
This also results in a nice invariant:
👆 that may seem like just fancy words, but reliable invariants like that are invaluable to me when working on complex projects, esp. on larger teams and with Remix beginners.
Fullstack components?
@kentcdodds wrote a fantastic blog post about fullstack components in Remix. The idea is to colocate a component that depends on API with the route that provides that API. Specifically, Kent recommends colocating the component and loader in the same file.
Notably, the component uses
useFetcher
and manually fetches data for the API route it depends on rather than usinguseLoaderData
as the whole point of a fullstack component is to use it anywhere, not just within routes nested under the route defining the loader. Indeed, "nesting" for resource routes isn't meaningful. As a result, the component depends on the API via that API's endpoint.For example, with the default file routing convention in v2, you can still get colocation of this code in the same directory:
The only import needed from
app/routes/api/blah/route.ts
is the type for the loader. Notably, user-definedtype
exports are fine since they are stripped before the code runs.Reasonable people can disagree, but I think this actually models the relationship between the route and the component more accurately. Just like any other part of my Remix code, if I need to access a loader from another route, I can
fetch
that endpoint and grab types from that route'sloader
by importing them.