The first release of 2023 is a big one for us. Remix 1.10 completes the React Router-ing of Remix and puts us in a solid position to ship some really exciting features in the new year. Let's dig in.
Rebuilt on React Router's data APIs
All of the data loading and mutation APIs you love in Remix are now completely built into the new framework-agnostic @remix-run/router
package. This layer serves as the foundation of both React Router and Remix, and it provides a canvas upon which new integrations can be built moving forward. The community has already started building some really exciting experiments with this, and we're cooking up a few cool things ourselves 🌶️.
As a Remix user, nothing about your code or its behavior should change. But the new implementation opens several new possibilities we think you'll love, and it allows us to quickly start bringing new React Router features into Remix (like sending promises over the wire 🤯).
And if you have a React Router app you've been thinking about migrating to Remix, you can be confident that using the new APIs in v6.4 will work the same way when you're ready to make the move (really though, we think you should make the move).
If you have any questions on these new APIs, head on over to their official documentation in the React Router docs.
Higher level control of revalidation
Exporting a shouldRevalidate
function from a route module gives you the ability to fine-tune how and when your route loaders are called.
Remix handles revalidation for you in many scenarios to keep your UI in sync with your data automatically. By default, route data is revalidated when:
- After an action is called from a
<Form>
,<fetcher.Form>
,useSubmit
orfetcher.submit
- When the URL search params change on the current route
- When dynamic route params change on the current route
- When the user navigates to the same URL
If shouldRevalidate
is exported from a route module, it will call the function before calling the route loader for new data. If the function returns false
, then the loader will not be called and the existing data for that loader will persist on the page. This is an optimization that can be used to avoid unnecessary database calls or other expensive operations.
// routes/invoices.jsx
export async function loader({ request }) {
let url = new URL(request.url);
let page = Number(url.searchParams.get("p") || 1);
let limit = 20;
return json(await getInvoices({ limit, offset: (page - 1) * limit }));
}
export function shouldRevalidate({ currentUrl }) {
// Submissions shouldn't trigger a reload on most navigations
// under `invoices`, so we only revalidate if the submission
// originates from the nested `/invoices/new` route
return currentUrl.pathname === "/invoices/new";
}
// routes/invoices.new.jsx
// The loader in `routes/invoices.jsx` will be revalidated after
// this action is called since the route's pathname is `/invoices/new`
export async function action({ request }) {
let invoice = await createInvoice(await request.formData());
return redirect(`/invoices/${invoice.id}`);
}
// routes/invoices/$invoiceId.jsx
// The loader in `routes/invoices.jsx` will *not* be revalidated after
// this action is called
export async function action({ request }) {
let invoice = await updateInvoice(await request.formData());
return json(invoice);
}
If you were already using unstable_shouldReload
, note that it is now deprecated in favor of shouldRevalidate
. Rename the export to shouldRevalidate
and update the function to match the stable API:
export function shouldRevalidate({
currentUrl,
currentParams,
nextUrl,
nextParams,
formMethod,
formAction,
formEncType,
formData,
actionResult,
defaultShouldRevalidate,
}) {
return true;
}
New hooks 🪝
useNavigation
When Remix was initially designed, React 18 and its concurrent features were still in the early stages of development, and its useTransition
hook had not yet landed. Now that it has, we decided to rename our useTransition
hook to useNavigation
to avoid naming conflicts and confusion. useTransition
will be deprecated in favor of useNavigation
with a slightly updated API:
let transition = useTransition();
let navigation = useNavigation();
navigation.state; // same as transition.state
navigation.location; // same as transition.location
// data flatted from transition.submission
navigation.formData; // any form data submitted with the navigation
navigation.formMethod; // 'GET' or 'POST'
navigation.formAction; // The action URL to which the form data is submitted
The type
property from useTransition
was confusing for many users, so we have removed it in useNavigation
. All of the information you need to inspect route transitions is available in useNavigation
in, we think, a much simpler and easier-to-understand interface. If you like the API for useTransition
and want to continue using it after we drop it in v2, I've written up our implementation in a Gist you can use in your projects.
useNavigationType
We've also exposed the useNavigationType
hook from React Router that gives you a bit more introspection into how the user is navigating.
// The user is navigating to a new URL.
useNavigationType() === "PUSH";
// The user is navigating back or forward in history.
useNavigationType() === "POP";
// The user is navigating but replacing the entry in
// history instead of pushing a new one.
useNavigationType() === "REPLACE";
useRevalidator
This hook allows you to revalidate data in your route for any reason. As noted above, Remix automatically revalidates the data after actions are called, but you may want to revalidate for other reasons. For example, you may want to revalidate data after a client-side interaction, in response to events from a websocket, or if the window is re-focused after the user re-activates the browser tab.
import { useRevalidator, useLoaderData } from "@remix-run/react";
function SomeRoute() {
let loaderData = useLoaderData();
let revalidator = useRevalidator();
useWindowFocus(() => {
revalidator.revalidate();
});
if (revalidator.state !== "idle") {
return <div>Revalidating...</div>;
}
return <div>{loaderData.superFresh}</div>;
}
useRouteLoaderData
This hook makes the data at any currently rendered route available anywhere in the tree. This is useful for components deep in the tree that need data from routes higher up, or parent routes that need data from one of its child routes.
// routes/invoices.jsx
import { Outlet, useLoaderData, useRouteLoaderData } from "@remix-run/react";
export default function Invoices() {
let allInvoices = useLoaderData();
let currentInvoice = useRouteLoaderData("routes/invoices/$invoiceId");
return (
<div>
<nav>
<h2>Invoices</h2>
{allInvoices.map((invoice) => (
<div key={invoice.id}>
<Link to={`/invoices/${invoice.id}`}>{invoice.name}</Link>
{currentInvoice?.id === invoice.id && (
<dl>
<dt>Amount</dt>
<dd>{invoice.amount}</dd>
<dt>Due Date</dt>
<dd>{invoice.dueDate}</dd>
</dl>
)}
</div>
))}
<main>
<Outlet />
</main>
</nav>
</div>
);
}
Other Notes
Here's a few other small changes to be aware of:
fetcher.load
calls now participate in revalidation - they always should have to avoid stale data on your page!<ScrollRestoration>
has a newgetKey
prop<Link>
has a newpreventScrollReset
prop