diff --git a/docs/releases/v0.19.0.md b/docs/releases/v0.19.0.md new file mode 100644 index 00000000000..b96df7804e4 --- /dev/null +++ b/docs/releases/v0.19.0.md @@ -0,0 +1,532 @@ +--- +published: 2021-10-07 +--- + +# v0.19.0 Release Notes + +Holy smokes this is a big release with tons of good stuff. Some let you handle new use-cases, some clean up your code, and others automatically make your website better and you don't have to do anything. This release puts us within inches of a stable v1. + +The biggest piece of work in this release is the rewrite of client side transitions. This enabled us to add a handful of new features, fix some bugs, and make it more efficient for the browser and faster for user at the same time. + +When the URL changes Remix does a bunch of communication with the server. We used to have a 300 line `useEffect` that just kind of did everything. We lovingly referred to it as "the big effect". We knew it was incomplete, but we were waiting to see how the rest of Remix shook out before really tackling this work. The time came and we spent months getting it right. Most of the features in this release are from that work or built on top of it. + +## tl;dr Upgrade Guide and Breaking Changes + +- `useRouteData` -> `useLoaderData` +- `usePendingFormSubmit` -> `useTransition().submission` +- `usePendingLocation` -> `useTransition().location` +- `block({ rel: "preload", as: "image", href })` -> Remove the block call, can render a `` wherever you link to the page +- `links({ data })` -> Use `` for `{ page }` links you used with data and then inline `` inside your component based on the `useLoaderData` instead. Most uses of `` are "body ok", so you can just render them inside the component instead. +- Returning a string from actions for a redirect need to actually return `redirect(string)` + +- [x] `useActionData` - actions can return data +- [x] Improved async handling with the new transition manager + - [x] same URL data reloading + - [x] hash change doesn't call loaders anymore + - [x] cancels stale loads/submissions + - [x] `useTransition` + - [x] `useFetcher` + - [x] `unstable_shouldReload` +- [x] `` (NavLink too) +- [x] `page` links now prefetch CSS resources + - [x] Breaking: no more `data` + - [x] Breaking: no more `block` +- [x] splat (catchall) route convention +- [x] CatchBoundary, `useCatch` +- [x] `
` +- [x] Now supports React Router beta 5 (and likely v6 final) +- [x] Automatically handle `HEAD` requests +- [x] Extend MDX plugins + +## Changes to actions + +Actions don't require you to redirect out of them anymore! You can return responses just like loaders now. The data you return is available from `useActionData()`. This is especially nice for server side form validation errors: just return the errors as an object, no more session/action/loader dance! + +```tsx +import { useActionData, json } from "remix"; + +export function action({ request }) { + let body = new URLSearchParams(await request.text()); + let name = body.get("visitorsName"); + return json({ message: `Hello, ${name}` }); +} + +export default function Invoices() { + let data = useActionData(); + return ( + +

+ +

+

{data ? data.message : "Waiting..."}

; +
+ ); +} +``` + +**Note about resubmissions**: Remix previously required redirects from actions to prevent accidental resubmissions (like booking a flight twice if the user clicks back). If you're rendering `` the form will not be resubmitted on back or refresh so you're still protected automatically. However, now that you aren't required to redirect out of actions, Remix can't protect your users from resubmissions when you aren't rendering ``. If you are handling forms without JavaScript, we highly recommend you still redirect out of your actions or ensure your actions can be run mutliple times without negative consequences. + +Finally, since actions can return data, returning a string will no longer automatically redirect, it will send down the string as data. You'll need to wrap it in `redirect(string)` when upgrading. + +[Read more about `useActionData`](https://docs.remix.run/v0.19.0-pre.0/api/remix/#useactiondata) + +## `useLoaderData` replaces `useRouteData()` + +Because "route data" can come from both loaders and actions now, `useRouteData` didn't make a lot of sense so we've got two hooks now: + +```js +useLoaderData(); // data from your loader +useActionData(); // data from your action +``` + +## `useTransition` replaces `usePendingLocation` and `usePendingFormSubmit` + +With the transition rewrite, we've got a better hook that ecompasses all "pending" information. This hook tells you everything you need to know to build even better loading experiences. For example, you can indicate all phases of the pending form submission to the user. Previous we only knew it was pending and nothing more, now you know everything. + +```js +function SubmitButton() { + let transition = useTransition(); + let text = + transition.type === "actionSubmission" + ? "Creating Record" + : transition.type === "actionRedirect" + ? "Redirecting to new record..." + : "Create"; + return ; +} +``` + +Updating from the old hooks is pretty straightforward: + +```js +// old +usePendingFormSubmit(); +// new +useTransition().submission; + +// old +usePendingLocation(); +// new +useTransition().location; +``` + +This hook also sets a solid foundation for us to finish our in-progress automatic scroll restoration, which should come very soon after this release. + +There are numerous improvements to client side transitions that don't affect your code, but make your app better. In the case of interrupted navigations and form submissions, Remix previously simply ignored the responses of stale navigation fetches. Now it automatically aborts them using `AbortController`, saving your user's network bandwidth and the browser doesn't waste CPU cycles processing the response. + +[Read more about `useTransition`](https://docs.remix.run/v0.19.0-pre.0/api/remix/#usetransition) + +## Same URL data reloading and hash changes + +Without JavaScript, if users click a link to the page they are already on, the browser will request a brand new document but replace the current entry in the history stack. Remix now emulates that behavior by refetching all loaders on the page and replacing the current entry in the history stack. + +We also fixed a bug where loaders were called when only the url hash was changing. URL hashes don't go to the server so they no longer cause loaders to be called either, but they are a new location. + +## `useFetcher` + +While Remix's loaders and actions are great for traditional navigations, modern apps often require more dynamic ways to communicate with the server. This hook enables you to call your loaders and actions _outside of a navigation_. You might think of it as using your loaders and actions as "API routes". Here are a few examples: + +- Writing a loader that returns data for a `` auto suggest component +- A newsletter sign up form at the bottom of multiple pages in your app +- Any UI where you need to allow multiple actions to be pending at the same time (like a list of records with single click buttons to change their state on the server) +- Components that fetch data based on user interactions rather than navigation, like a user avatar that pops up their profile when hovered or focused. + +Here's an example of marking an article as read: + +```tsx +function useMarkAsRead({ articleId, userId }) { + let markAsRead = useFetcher(); + + useSpentSomeTimeHereAndScrolledToTheBottom(() => { + markAsRead.submit( + { userId }, + { + method: "POST", + action: `/article/${articleID}/mark-as-read`, + } + ); + }); +} +``` + +After the action completes, Remix will do its normal thing of reloading all loaders on the page after actions to ensure the data shown to the user is the latest data from the server. If multiple actions are pending at the same time, Remix makes sure to commit every fresh respnose and aborts any stale ones. That's right, Remix automatically takes care of race conditions! + +Additionally, if you return a redirect from a loader/action being called by a fetcher, Remix will redirect the application to that page. And if any errors are thrown, the nearest error boundary will be rendered as usual. With `useFetcher` you get all of the same protections as a normal navigation when communicating with the server. + +There are a lot more examples in the docs you should go check out: + +[Read more about `useFetcher`](https://docs.remix.run/v0.19.0-pre.0/api/remix/#usefetcher) + +## `unstable_shouldReload` + +During client side transitions, Remix will optimize reloading of routes that are already rendering, like not reloading layout routes that aren't changing. In other cases, like form submissions or search param changes, Remix doesn't know which routes need to be reloaded so it reloads them all to be safe. This ensures data mutations from the submission or changes in the search params are reflected across the entire page. + +This function lets apps further optimize by returning `false` when Remix is about to reload a route. The most common case is telling Remix to never reload the root route: + +```js +export let loader = () => { + return { + ENV: { + CLOUDINARY_ACCT: process.env.CLOUDINARY_ACCT, + STRIPE_PUBLIC_KEY: process.env.STRIPE_PUBLIC_KEY, + }, + }; +}; + +export let unstable_shouldReload = () => false; +``` + +As always, Remix puts you in charge of the network tab. + +[Read more about `shouldReload`](https://docs.remix.run/v0.19.0-pre.0/api/remix/#unstable_shouldreload) + +## Link Prefetching + +This feature is _awesome_. One of our goals with Remix is to "destroy all spinners". Of course, we have a really great API to help you build great loading UI (`useTransition`), but the end goal is to not need the spinner in the first place. You can do that by link prefetching: + +```js +import { Link, NavLink } from "remix" // not react router! + +// prefetch resources when the user seems like they're going to click it + + +// prefetch it when this link renders + +``` + +We recommend covering your app with ``. Because of nested routes, Remix is able to prefetch, in parallel: + +- The JS modules for next matching routes in the link +- The CSS from the `links()` export of those routes +- All the loader data for the next routes + +Under the hood it uses `` so browsers can do everything they should (rather than other solutions that use their own framework level fetching and caching). For example, cache headers on your loaders will automatically be respected by the browser and chrome even prefetches these resources for the back and forward buttons after you click away from the page 🤯. Users can even refresh the browser or change tabs and the browser cache for any prefetched resources will still be available. What was that hash tag from a few years ago? #useThePlatform. + +## `{ page }` links now prefetch CSS + +Before this release, the only way to prefetch a page was to include it in the `links()` export of a route. Link preloading makes the `{ page }` link less interesting and there's a chance we'll remove it from the Remix v1. + +But while it's still with us, it now prefetches CSS resources for the linked page! + +## Removal of data in `links({ data })` + +In order for `` to be able to prefetch the CSS of the next page, we had to remove the `data` argument to `links`. We don't actually fetch the data, we tell the browser to do it with ``, so we don't actually have the data to be able to pass to `links()` when prefetching. Because of this we had a choice: + +1. Not be able to prefetch css and making it impossible to eliminate spinners on transitions to routes with links +2. Remove the data arg and be able to prefetch all resources for the next page ahead of time, and in parallel + +The main reason we provided the data arg in the first place was to prefetch pages based on data with `{ page }` links. You can now do exactly that with `` so we feel comfortable removing this feature. + +If you were using `data` for more than `{ page }`, like `{ rel: "preload" }` you will probably be able to do the prefetching of those resources on the page that links to the route that used to have the link preloads. + +```js +// old - routes/users/$userId.js +export function links({ data }) { + return data.map((user) => ({ + rel: "preload", + as: "image", + href: user.avatarUrl, + })); +} + +// new - note this is not in the $userId.js route, it's wherever you're linking +// to the $userId route +export default function SomeComp() { + let users = useLoaderData(); + return ( +
+ {users.map((user) => ( + + {user.name} + {/* Prefetch it where you linked to it, + you probably have the data you need */} + + + ))} +
+ ); +} +``` + +We recognize this is a bit of a bummer, but we couldn't eliminate spinners, and fetch more resources in parallel, without removing the data arg to links and we're confident you can still prefetch those resources in another way. + +## Removal of `block` in `links` + +This feature allowed you to block the transition to a route on any linked resource in that route. The primary motivation was blocking on critical images to avoid content layout shift when you got there. Unfortunately, this only worked for client side transitions so users still experienced content layout shift on the initial page load of a page--which is actually the most important time to avoid CLS. + +Since block could only solve half the problem, and blocking on images is generally a bad idea anyway (there's a reason browsers don't block on images for the initial load) we removed it to encourage developers to solve the root of the problem: put a height and width on your images :) + +Any other resources you'd want to block on are already handled by Remix: JS modules, data, and CSS resources, and with link prefetching we'd like to block on as little as possible. + +## Catch Boundaries and `useCatch()` + +In addition to returning responses from loaders and actions, you can now _throw responses_ and like thrown errors, Remix will change its rendering path from the route component to the `CatchBoundary`. Check it out: + +```tsx +import { useCatch, json } from "remix"; + +export async function loader({ request, params }) { + let userId = await requireUserSession(request); + let project = await fakeDb.project.find({ + where: { id: params.id }, + }); + + // if at any point you can't render this route because you don't have the + // right data, you can throw a response, code stops executing and Remix takes + // the app down the "Catch Boundary" path. + if (project === null) { + throw new Response("", { status: 404 }); + } + + // you can even include data in the response to tell the user how to fix the + // problem + if (!project.members.includes(userId)) { + throw json( + { ownerEmail: project.ownerEmail }, + { status: 401 } + ); + } + + // but if everything is good, continue on the happy path! + return json(project); +} + +export function CatchBoundary() { + let caught = useCatch(); + + if (caught.status === 404) { + return
Project not found.
; + } + + if (caught.status === 401) { + return ( +
+

+ You don't have access to this project. Email{" "} + {caught.data.ownerEmail} to request access. +

+
+ ); + } +} + +export function Project() { + // you know everything worked on the server, no need to handle not found, no + // access, etc. in your component. This is the happy path that Remix only + // sends you down if everything worked on the server. + let project = useLoaderData(); + return ; +} +``` + +When you throw a response from a loader, it bubbles just like error boundaries bubble, so any loader in your app can throw a 404 and it will bubble up to the nearest `CatchBoundary`. This means you can have granular 404 handling without taking out all of the UI on the page, as well as global handling by setting up a `CatchBoundary` at the top of your app.Any loader can throw a 404 and your root catch boundary will handle it if nobody else does in-between. + +We recommend you copy/paste/tweak this into your `src/root.js` file (note that this is the `` root of your app, so if you don't have a `` component like our `remix init` templates, make sure to include the entire html page you need): + +```tsx +// in src/root.js +export function CatchBoundary() { + let caught = useCatch(); + + switch (caught.status) { + // add whichever other status codes you want to handle + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses + case 401: + case 404: + return ( + +

+ {caught.status} {caught.statusText} +

+
+ ); + + default: + throw new Error( + `Unexpected caught response with status: ${caught.status}` + ); + } +} +``` + +### Removal of `routes/404.js` + +Because we now have catch boundaries, all 404 handling is done with that. You can move the code from your `404.js` file into the `root.js` `CatchBoundary` component. + +## Throwing Redirects + +Along with catch boundaries introducing the ability to throw responses, any thrown redirect response will redirect the app. This is huge for writing cleaner code in your loaders and abstractions. For example, instead of the "pyramid of death" that was required before (with a callback "push" API), you can now write cleaner loaders and actions that throw instead of returning internally. + +For example, consider the `requireUserSession` case. + +```js +// OLD +// src/utils/session.js +async function requireUserSession(request, cb) { + let cookie = request.headers.get("Cookie"); + let session = await getSession(cookie); + let auth = session.get("auth"); + + if (!auth) { + return redirect("/login"); + } + + return cb(auth); +} + +// src/routes/some-route.js +export async function loader({ request }) { + // loaders had to return the call to this function so it could internally + // return a redirect instead of calling our callback and returning that. It's + // tricky code to write and understand. + return requireUsersSession(request, async (auth) => { + let project = await getProject(); + return project; + }); +} + +// NEW +// src/utils/session.js +async function requireUserSession(request) { + let cookie = request.headers.get("Cookie"); + let session = await getSession(cookie); + let auth = session.get("auth"); + + if (!auth) { + // stop executing code, there's no reason to keep going because we want to + // go somewhere else + throw redirect("/login"); + } + + // otherwise just return the auth, no callback higher-order-function-academics + return auth; +} + +// src/routes/some-route.js +export async function loader({ request }) { + // simply await auth, if it's not there, code will stop executing and the user + // will be redirected + let auth = await requireUsersSession(request); + let project = await getProject(); + return project; +} +``` + +This is really nice when you have multiple utilities in play: + +```js +// OLD +export let loader = async ({ request }) => { + return removeTrailingSlash(request.url, () => { + return withSession(request, (session) => { + return requireUser(session, (user) => { + return json(user); + }); + }); + }); +}; + +// NEW +export let loader = async ({ request }) => { + removeTrailingSlash(request.url); + let session = await withSession(request); + let user = await requireUser(session); + return json(user); +}; +``` + +It's very similar to how React hooks elminated the "pyramid of death" with render props by changing from a "push api" (the data is pushed to a callback) to a "pull api" (the data is simply returned). It not only cleans up your loaders, but makes it easier to compose different loader utilities together. + +## Splat route file convention + +React Router has always supported routes ending in `*` but the only way to do it in Remix was with `remix.config.js`. Now you can define a "splat route" by naming the file `$`. For example: + +- `routes/$.js` - will match everything that doesn't match another route +- `routes/docs/$version/$.js` - will match everyting under urls like `docs/v0.19/guides/installation` and will be nested under `routes/docs/$version.js`. +- `routes/files.$.js` - will match all urls under `files/` without any nesting since it's a `.` instead of a nested folder. + +## Layout Route file convention + +Routes in React Router (and therefore Remix) add both segments to the URL and layouts to the UI. Sometimes you need a segment without a new layout, other times you want a layout without a new segment. You can now add layouts without adding any path segments. In React Router it looks like this: + +```js + + }> + } /> + } /> + } /> + + }> + } /> + } /> + + +``` + +At "/contact", React Router will render ``. At "/dashboard", React Router will render ``. You'll notce two routes up there add layouts, but they don't add any path segments to the URL. They are essentially ignored for matching but used while rendering. + +In Remix you can now configure these "layout routes" by prefixing your route (and its child-routes folder) with an underscore like `_public`. From there it works like any other nested routes, they just don't add path segments to the URL. + +``` +└── routes + ├── _authenticated.js + ├── _authenticated + │   ├── dashboard.js + │   └── projects.js + ├── _public.js + └── _public + ├── contact.js + ├── index.js + └── login.js +``` + +## `?index` in form actions + +Appending `?index` to a `
` tells Remix to post to the `routes/projects/index.js` route rather than the parent route at `routes/projects.js`. + +We had a strange case where simply defining an index route under a parent route would completely change the behavior of the app. It was especially confusing when you leave off the action completely like ``. If you did _not_ have an index route, it would post to the parent route as expected. If you added an index route under the parent, it would suddenly start posting there! + +Now, if you use a `` inside of a parent route, it will post to the parent. If it's in the index route without an action, it will post to the index route. When you're defining the action, you can tell Remix which route to post to with `?index` + +```js +// posts to src/routes/projects.js + + +// posts to src/routes/projects/index.js + + +// posts to which ever route in which it's rendering + +``` + +## React Router `v6.0.0-beta.6` + +Remix is now compatible with React Router `v6.0.0-beta.6`. We're days away from launching the stable v6 release over there! + +## Extending MDX Plugins + +You can now provide plugins to MDX in remix.config: + +```js +exports.mdx = async filename => { + return { + remarkPlugins: [require("remark-toc"), + rehypePlugins: [require("rehype-highlight")] + }; +}; +``` + +## HEAD requests + +Remix now automatically handles `HEAD` requests that come to your server. They're exactly like GET, except the client is asking for just the HTTP headers. Remix does everything it normally does, but at the end strips the body from the response (it has to do a normal render of the app in order for headers like "content-length" to be accurate). + +Holy smokes. That's quite enough for one release! Enjoy!