Lots of bug fixes, some new features, and we ALMOST made it w/o a breaking change, but there is one, it's super easy though.
This is the only thing you have to do to upgrade from v0.12.x
:
app/entry-browser.js
to app/entry.client.js
(or .tsx
)app/entry-server.js
to app/entry.server.js
(or .tsx
)This brings our file naming conventions in alignment with one of the new features in this release.
We haven't talked about this very much publicly, but generally speaking the Remix compiler does a decent job at deciding which modules to include in your browser bundles vs. which are meant only for the server. It does this through a feature known as "tree-shaking" that permits the compiler to remove dead code from the output bundles.
Let's say you have a module that contains a few functions for accessing your backend database. You could import
this module into one of your route modules so you can use it in your loader
and/or action
, like this:
import { useRouteData } from "remix";
import { json } from "@remix-run/data";
import { db } from "../database";
export async function loader({ params }) {
let user = await db.select(
"users",
(where: { userId: params.userId })
);
return json({ user });
}
export function MyPage() {
let { user } = useRouteData();
// ...
}
At compile time, we can see that the only place you're using anything from ../database
is in your loader
, so when we build the client bundle we can remove that code entirely ("tree-shake" it) from the build. This includes both the loader
function itself, as well as the import
of ../database
!
This works great most of the time, but sometimes you get into weird situations where the compiler can't automatically infer which files it needs only on the server, or only on the client. For these times, we provide an escape hatch: *.client.js
and *.server.js
.
For example, on our own website we got into a situation where we were importing both firebase
and firebase-admin
in our /login
route. We use the firebase
package in the component code to create the user session, and we use the firebase-admin
package in the loader
(on the server) to verify and create the cookie. Our code looked something like this:
import admin from "../utils/firebaseAdmin.js";
import firebase from "../utils/firebase.js";
export function loader() {
// use `admin` in here
}
export function LoginPage() {
function loginFormHandler() {
// use `firebase` in here
}
// ...
}
The firebase
package isn't really meant to run on the server--it's client-only. But we can't easily infer that it's not needed in the server bundles because of the way it's used in an event handler. So instead, we use the .client.js
file extension on our utils/firebase.js
to exclude it from the server build!
All we need to do is change our filename:
import firebase from "../utils/firebase.client.js";
Now utils/firebase.client.js
won't ever end up in the server bundles.
So that's the feature in a nutshell: use .server.js
(or .server.tsx
) as your file extension when you know a file is only ever meant to be run server-side, or use .client.js
when it's only ever meant to run in the browser. And remember, most of the time the compiler should automatically be able to figure it out for you, so this is really just an escape hatch!
You can now import CSS with the css:
import assertion. It's just like url:
except that the file will be processed with PostCSS (as long as you have a postcss.config.js
file in the Remix app root).
// <app root>/postcss.config.js
module.exports = {
plugins: [require("autoprefixer"), require("cssnano")]
};
// <app root>/routes/some-route.js
import style from "css:../styles/something.css";
// usually used with links
export let links = () => {
return [{ rel: "stylesheet", href: style }];
};
You can find a few PostCSS setups in the styling docs.
Note: Using this plugin will slow down your builds. Remix won't rebuild a file that hasn't changed, even between restarts as long as you haven't deleted your browser build directory. It's usually not a big deal unless you're using tailwind where it's common for 5-20 seconds to build a file the first time depending on your tailwind config.
useMatches
hook and Route Module handle
exportRemix internally knows the all of the routes that match at the very top of the application hierachy even though routes down deeper fetched the data. It's how <Meta />
, <Links />
, and <Scripts />
elements know what to render.
This new hook allows you to create similar conventions, giving you access to all of the route matches and their data on the current page.
This is useful for creating things like data-driven breadcrumbs or any other kind of app convention. Before you can do that, you need a way for your route to export an api, or a "handle". Check out how we can create breadcrumbs in root.tsx
.
First, your routes can put whatever they want on the handle
, here we use breadcrumb
, it's not a Remix thing, it's whatever you want.
// routes/some-route.tsx
export let handle = {
breadcrumb: () => <Link to="/some-route">Some Route</Link>
};
// routes/some-route/some-child-route.tsx
export let handle = {
breadcrumb: () => (
<Link to="/some-route/some-child-route">
Child Route
</Link>
)
};
And then we can use this in our root route:
import {
Links,
Scripts,
useRouteData,
useMatches
} from "remix";
export default function Root() {
let matches = useMatches();
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<Links />
</head>
<body>
<header>
<ol>
{matches
// skip routes that don't have a breadcrumb
.filter(
match =>
match.handle && match.handle.breadcrumb
)
// render breadcrumbs!
.map((match, index) => (
<li key={index}>
{match.handle.breadcrumb(match)}
</li>
))}
</ol>
</header>
<Outlet />
</body>
</html>
);
}
A match looks like:
interface {
// The amount of the URL this route matched
pathname: string;
// whatever your route's loader returned
data: any;
// the parsed params from the url
params: { [name: string]: string };
// the handle exported from your route module
handle: any;
}
We're excited to see what conventions you come up with!
action
to usePendingFormSubmit()
FormData
, URLSearchParams
) with useSubmit
meta
functionEnjoy!