Releases: remix-run/remix
v0.11.0
Sessions and Cookies
This release introduces first-class support for sessions and cookies at the route level instead of piggy-backing on framework middleware like express-session
.
Background
We initially thought we could just build out our sessions API on top of existing frameworks and cloud providers. After all, why reinvent something that is already working well for people? One of the core goals of Remix is to play well with your existing stack and technologies that you already know and love.
However, we quickly discovered that not all cloud providers support session management. To some of them it's a detail that's best left up to the app developer. That's a fair position, but it means that in order to provide a consistent experience in Remix when it comes to handling sessions, we had a bit of work to do.
Another reason we weren't comfortable with this approach is that in many node server frameworks, sessions rely on using middleware. To use sessions in Express, you insert some middleware into your stack in one spot and it runs for multiple different request handlers.
While there's nothing inherently bad about this model, our goal with Remix is to push all functionality for handling a request into your route loader
and action
functions. This means each one will have a bit more work to do, but it also means it's completely isolated from other request handlers in your app, which is closer to the lambda model. Our hope is that by enforcing an architecture that completely encapsulates the request/response cycle at the route level it will be easier to eventually deploy Remix loaders and actions to individual cloud functions, if desired.
That being said, remember your escape hatch is always getLoadContext
. So if you really want to use something from your cloud provider or framework (like an Express middleware) and get it through to your loaders/actions, we completely support that.
Session storage
The major new API in this release is the SessionStorage
interface. A session storage object is responsible for parsing/serializing HTTP cookies and managing persistence of session data between requests.
SessionStorage
has three methods:
getSession
- Returns theSession
object associated with a HTTP cookie, or a new session if there was noCookie
headercommitSession
- Saves session data to storage and returns the value to use for the response'sSet-Cookie
headerdestroySession
- LikecommitSession
, but destroys all session data instead of saving it
Remix provides three built-in session storage options:
createFileSessionStorage()
- Stores session data on the filesystem, ala PHPcreateCookieSessionStorage()
- Stores session data in the session cookie itself. This is useful when you don't have a filesystem or don't want to use a database for some reason.createMemorySessionStorage()
- A lightweight development/testing-only session storage for testing your session-based user flows
The docs on sessions contain a comprehensive example of how you might build a simple user authentication form using the new API.
The docs also include an example of building a custom session storage using the new createSessionStorage()
API, which is designed to let you build a session storage solution backed by any database you want.
Cookies
Of course, a core component behind sessions is the ability to handle HTTP cookies. Remix v0.11 also includes a Cookie
interface and createCookie()
function that help when dealing with cookies generally.
The idea behind the Cookie
API is that you create reusable cookie
objects that know how to parse and serialize cookies, complete with support for rotating secrets used for signing cookies to verify their integrity (important when it's a session cookie).
Changes to server endpoints
This is mostly an internal change, but in order to make cookies work better in your loaders and actions, we've made a few subtle changes to the server endpoints. Specifically, the /_remix/data
endpoint is now just the regular route endpoint with a ?_data
parameter in the query string. This means when you set a path
on your cookies to the path of one of your routes, you'll get that cookie in your loader/action as well.
The /_remix/manifest
endpoint also changed to the same as the route endpoint, but with a ?_manifest
parameter in the URL query string. This is for consistency with the data endpoint, but may also prove helpful in the future for specifying additional security around the manifest endpoint.
In addition to these usability benefits, it tends to clean up the Network tab quite a bit as well :D
Upgrading
Upgrading to v0.10 should be zero work if you weren't using sessions. If you were, take the following steps:
- Create your own session storage object in
app/sessionStorage.ts
(orapp/sessionStorage.js
if you're using JavaScript) - Remove the
session
argument in yourloader
andaction
functions and use thegetSession
andcommitSession
functions from your session storage object instead - For
@remix-run/express
- Remove your express session middleware since we're not using it anymore
And that should be just about it. Please let us know if you encounter any issues upgrading.
As always, thank you for your support!
v0.10.0
Root Layout Route
Background
The App.tsx
file has been a little bit goofy. This release cleans that up. Instead of needing to import the app file into both entry-server
and entry-client
, we've now got the concept of a "root route". This root route works like all others, it can export a loader and error boundary, etc.
This small change removes a few APIs and a lot of complexity in Remix's internals. It was one of those -200 line changes without losing any features, which always feels amazing.
Upgrading:
Move App.tsx
to root.tsx
Move your App.tsx
to root.tsx
. "root" has special meaning, so it must be named that.
Change <Routes/>
to <Outlet/>
Now that root.tsx
is a route, you render an Outlet just like every other layout route.
// OLD
import { Routes } from "remix";
export default function App() {
return (
<html>
{/* ... */}
<body>
<Routes />
</body>
</html>
);
}
// NEW
// Note it's from "react-router-dom"!
import { Outlet } from "react-router-dom";
export default function App() {
return (
<html>
{/* ... */}
<body>
<Outlet />
</body>
</html>
);
}
Move global data code
Because root.tsx
iss now a route it can export a loader for it's own data. If you had a global-data.ts
file, cut and paste the code into root.tsx
so it's co-located like all of your other routes.
Also, there is no longer a useGlobalData
hook because this is now normal route data. So change your useGlobalData()
to useRouteData()
.
Update your server and browser entries
Don't import or render App.tsx
in entry-server
or entry-browser
anymore:
// OLD: entry-server
import App, { ErrorBoundary } from "./App";
let markup = ReactDOMServer.renderToString(
<Remix context={remixContext} url={request.url} ErrorBoundary={ErrorBoundary}>
<App />
</Remix>
);
// NEW: entry-server
// - no more App import because you moved it to `root.tsx`
// - no more ErrorBoundary prop because the root route exports
// it's own error boundary like any other route
let markup = ReactDOMServer.renderToString(
<Remix context={remixContext} url={request.url} />
);
Do the same thing in entry-browser.tsx
, it'll end up looking something like this:
import ReactDOM from "react-dom";
import Remix from "@remix-run/react/browser";
ReactDOM.hydrate(<Remix />, document);
Much nicer! No more special casing the root layout. We're also thinking this opens up the possibility for multiple root layouts: think signed-in-layout and signed-out-layout". We're not there yet though :)
Importing images
You can now import and resize images as a JavaScript module containing links to the assets, height and width attributes, and even responsive image source sets. Check it out!
// - change quality to 50
// - reformat to avif
// - generate a server rendered Base64 blurry placeholder
// - resize to 500
import guitar from "img:./guitar.jpg?quality=50&format=avif&placeholder&width=500";
// - change quality to 80
// - generate 3 responsive image sizes and a srcset for the `<img srcSet/>`
// - generate a ssr placeholder
import guitar2 from "img:./guitar.jpg?quality=80&srcset=720,1080,2048&placeholder";
export default function Guitar() {
return (
<div>
<p>Fixed Image</p>
<img
alt="Guitar"
src={guitar.src}
style={{
backgroundImage: `url(${guitar.placeholder})`,
backgroundSize: "cover"
}}
width={guitar.width / 2}
height={guitar.height / 2}
/>
<p>Responsive</p>
<img
alt="Guitar"
src={guitar2.src}
srcSet={guitar2.srcset}
style={{
backgroundImage: `url(${guitar2.placeholder})`,
backgroundSize: "cover"
}}
/>
</div>
);
}
v0.9.0
This is another major change (like 0.8) that is focused on improving the developer experience in Remix. It's a major change because it essentially changes one of our core assumptions about how people would be using Remix based on feedback we've received since we launched our supporter preview in October.
tl;dr: In version 0.9, the data
directory is gone (as is the dataDirectory
export in remix.config.js
). Instead, put your loader
and action
functions right there in your route modules (in app/routes
) next to your component, headers
, and meta
. Remix will automatically compile builds for both the server (for server rendering) and the browser. data/global.js
is now app/global-data.js
.
Background
One of our main goals with Remix is that it doesn't have to own your entire stack. Sure, you could build an entire app on Remix. But if you have an existing node server, you don't have to abandon it or port everything to a new codebase when you decided to adopt Remix for your frontend. In line with this goal, Remix provides several different packages for working with various cloud providers including Architect (AWS Cloud Formation) and Vercel, and we are hard at work on many more.
We also assumed that, since Remix fits into your existing stack, we wouldn't have to handle compiling any of your backend code since you'd probably already have a build process in place. So we provided a data
directory for all backend code. While it's technically possible to compile the backend code yourself, what this means in practice is that in order to use Remix you have to set up a separate build for data
. And you probably don't already have a data
directory because before Remix came along you didn't structure your code like that.
Additionally, many people are using TypeScript these days (we are!) and it's inconvenient to have separate folders for your data loaders and components when they use the same types! This caused a few of you to create a root-level types
directory just so you could share code between data
and app
.
So, the assumption was that we didn't need to handle anything in the data
directory, but based on your feedback we can see clearly this needs to change!
Improvements
As was mentioned previously, the data
directory is gone in 0.9.Instead of putting your loader
and action
functions in data/routes
, move those functions into the same corresponding files in app/routes
alongside your route components, headers
, and meta
functions. If you had a data/global.js
file, move it to app/global-data.js
. Then go delete your data
directory and your dataDirectory
export in remix.config.js
.
When remix run
or remix build
runs, Remix will generate two versions of your modules: one for the server (for server rendering) and one for the browser. For the browser build, Remix will automatically strip out any code that isn't meant to run in the browser. This means that server-only code in your loader
and action
functions (and any import
s that are used only by them) won't appear anywhere in the browser bundles.
So if you had this in data/routes/team.ts
:
import type { Loader } from "@remix-run/data";
import { db } from "../db";
export let loader: Loader = async () => {
return await db.query("select * from team");
};
Go ahead and move that code into app/routes/team.tsx
:
import { useRouteData } from "remix";
import type { Loader } from "@remix-run/data";
import { db } from "../db";
export let loader: Loader = async () => {
return await db.query("select * from team");
};
interface TeamMember {
name: string;
}
type Team = TeamMember[];
export default function MeetTheTeam() {
let team = useRouteData<Team>();
return (
<div>
<h1>Meet the Team</h1>
<ul>
{team.map(member => (
<li>{member.name}</li>
))}
</ul>
</div>
);
}
Now everything you need for a single route is in one spot; the data and the view. And don't forget you can always associate custom headers
and/or meta
information with a route in this file as well.
We've been using this already on our own projects internally and it feels really great to have everything in one spot. It's difficult to overstate the importance of avoiding context switching when working with code in order to move quickly and feel productive. One of the core innovations of React was keeping the state right there in the view, which made it feel incredibly productive almost immediately. We feel like this is a similar advantage of having everything in the same file in a route.
You might also feel like this makes it a little more tricky to think about what code in this file is going to run on the server and what code is going to run in the browser. But this isn't something new. Your components have always run on both the server and in the browser. That's just one of the trade-offs of server rendering! We are hoping that it will be easy enough to just remember that anything in loader
and action
won't make it to the browser.
Implementation Notes
You might be wondering how this all works behind the scenes, since any import
s of server-only libraries like @prisma/client
or firebase-admin
aren't ever supposed to run in the browser.
To build this feature, we relied heavily on Rollup's built-in tree-shaking capabilities. When we run the browser build, we tell Rollup to ignore the loader
and action
methods in the output. We also tell it to ignore any module-level side effects, like database client initialization logic, so it aggressively prunes out the imports of any code in the module graph that is used only in loader
and/or action
.
Simpler TypeScript Setup
With the data directory, Remix wasn't compiling your TypeScript. This led to "two builds" in Remix. The application, not Remix, had to build TypeScript for the modules in your data directory, then Remix built TypeScript in your app. This made sharing code overly complicated and was just not as nice as having one build to worry about.
Because loaders/actions are inlined with your route modules, you no longer need a separate TypeScript build for data modules. If you used one of our starters, you can:
- remove all of the TypeScript build and config from your app. In package.json you probably have some
tsc -b
andtsc -w
code, you can remove it - get rid of all of your
tsconfig.json
files exceptapp/tsconfig.js
.
In the Overview Video, you can see all of the places affected by this. We're really happy with this change because it simplifies a lot of things for your application and for Remix itself.
Error Boundaries
We are also including first-class support for error boundaries in this release. Of course, you've always been able to use React 16's built-in error boundaries in Remix, but we have taken it one step further and introduced a top-level ErrorBoundary
export in your route modules.
An ErrorBoundary
is a component that is rendered in place of your route component whenever there is an error on that route, whether it be a render error (from inside one of your route components) or an error thrown from inside one of your loaders or actions.
In addition to supporting error boundaries at the route level, we also include support for a global ErrorBoundary
as a prop to your <Remix>
element. All of our starter repos have been updated to show how this is to be done (see app/entry-browser.tsx
and app/entry-server.tsx
).
Instead of using app/routes/500.tsx
for your global error handler, Remix will now use your global ErrorBoundary
component. It will still automatically change the HTTP response status code to 500. Since this functionality is only for real errors (uncaught exceptions), 500 is the appropriate status code.
Upgrading Summary
- Move
data/global.ts
toapp/global-data.ts
- Delete the
dataDirectory
config inremix.config.js
- Copy/paste the code in
data/routes/<data-module>.ts
to its correspondingapp/routes/<route-module>.tsx
file. - Delete your
app/routes/500.tsx
file - (optional) Add
ErrorBoundary
components to your routes and/or your top-level<Remix>
element - If using one of our starter templates with TypeScript, remove all TypeScript build/config code except
app/tsconfig.js
v0.8.0
This release is pretty significant and we're really excited about it. It sets the foundation for everything else we want to do with Remix. Hang on tight though, there are a lot of changes, and we appreciate your patience as we shift the API around a bit during this preview period. After our production release we'll have proper, backward-compatible API deprecation, but right now we're prioritizing getting Remix stable.
We recommend running through the tutorial with fresh eyes again to capture all of these changes.
Improvements
<Form>
component and Actions
While previously "loaders" allowed you to load route data, "Actions" coupled with <Form>
allow you to make changes to data with the simplicity of old-school forms posts but the progressive enhancement of React.
<Form method="post" action="/projects">
<p>
<label>
Name: <input name="name" type="text" />
</label>
</p>
<p>
<label>
Description: <textarea name="description" />
</label>
</p>
<p>
<button type="submit">Create</button>
</p>
</Form>
And the action that handles the post:
import type { Action } from "@remix-run/data";
import { parseFormBody, redirect } from "@remix-run/data";
let action: Action = async ({ request }) => {
let newProject = parseFormBody(request);
let project = await createProject(newProject);
return redirect(`/projects/${project.id}`);
};
export { action };
Finally, you can make the interaction fancy with usePendingFormSubmit()
for loading indication and optimistic UI:
import { usePendingFormSubmit } from "remix";
function SomePage() {
let pendingSubmit = usePendingFormSubmit();
if (pendingSubmit) {
return (
<div style={{ opacity: 0.5 }}>{pendingSubmit.data.get("title")}</div>
);
} else {
return (
<Form>
<input name="title" />
<button type="submit">Create</button>
</Form>
);
}
}
Read more:
Added usePendingLocation
This hook gives you the next location so you can match on its pathname for contextual loading indication on links and more
let nextLocation = usePendingLocation();
console.log(nextLocation && nextLocation.pathname);
Read More:
Added parseFormBody
Now that we have <Form>
and Actions, you need a way to parse the form's request body.
import { parseFromBody } from "@remix-run/data";
let action = ({ request }) => {
let body = parseFormBody(request);
};
It returns a URLSearchParams
or FormBody
depending on the encType
of the form, both objects work almost identically though:
Read more:
Request object passed to loaders and actions
Instead of passing a URL, we pass the whole Request object so you can read the method, parse the body, etc.
let loader = ({ request }) => {
request.method;
request.url;
let url = new URL(request.url);
url.get("some-param");
// etc.
};
Sessions
Remix platform wrappers like @remix-run/express
can detect when you've enabled sessions for your app and automatically send a remix session
object to loaders and actions. This is great for storing flash messages about actions that just happened on the server across your app or storing form validation errors to display on the next page.
let action = async ({ params, session }) => {
let deletedProject = await archiveProject(params.projectId);
session.flash(
"globalMessage",
`Project ${deletedProject.name} successfully archived`
);
return redirect("/dashboard");
};
// data/global.ts
let loader = ({ session }) => {
let message = session.get("globalMessage") || null;
return { message };
};
// app/App.tsx
export default function App() {
let { message } = useGlobalData();
return (
<html>
<head>
<Meta />
<Styles />
</head>
<body>
{message && <div>{message}</div>}
<Routes />
<Scripts />
</body>
</html>
);
}
Read More:
Importing .json
files
You can now import .json
files just like in Node.
import json from "./something.json";
console.log(json);
.ts
and .tsx
for routes/404
and routes/500
Previously they had to be .js
.
Breaking Changes
Renamed @remix-run/loaders
to @remix-run/data
Also, the remix config name for your loaders changed from loadersDirectory
to dataDirectory
.
While this is configurable, we also changed the default folder from "loaders" to "data" in the starter templates, and all docs now talk about the "data" folder instead of "loaders".
We made this change because your data loaders can now export two functions: loader
and action
. So it didn't make sense to call them "loaders" anymore but "data modules". So a "data module" can export a "loader" and and "action". Data modules live in data/routes/**/*{.js,.ts}
.
In your data modules (previously "loaders"):
// old
import { json } from "@remix-run/loader";
// new
import { json } from "@remix-run/data";
In your remix.config.js
// old
exports.loadersDirectory = "./loaders";
// new
exports.dataDirectory = "./loaders";
// or if you want to be more semantic with the changes here, just make sure to
// rename the folder!
exports.dataDirectory = "./data";
Removed @remix-run/notFound
It wasn't that helpful and gave people the wrong idea.
// old:
import { notFound } from "@remix-run/loader";
module.exports = () => {
return notFound();
};
// new:
exports.loader = () => {
return new Response("", { status: 404 });
};
Please note that this does not render the routes/404
component, it renders whatever matched so you'll probably want to send some extra information down so your UI can handle it better.
// old:
import { notFound } from "@remix-run/loader";
module.exports = () => {
return notFound();
};
// new:
import { json } from "@remix-run/loader";
exports.loader = () => {
return json({ notFound: true }, { status: 404 });
};
Then you can read that data from useRouteData()
and render a contextual not found page with the matching component.
Removed loader url
You can use the request object to create a url:
// old
let loader = ({ url }) => {
let param = url.searchParams.get("foo");
};
// new
let loader = ({ request }) => {
let url = new URL(request.url);
let param = url.searchParams.get("foo");
};
Removed useLocationPending
in favor of usePendingLocation
// old
let pending = useLocationPending();
// new
let nextLocation = usePendingLocation();
// or coerce to boolean and ensure identical behavior to your old code:
let nextLocation = usePendingLocation();
let pending = !!nextLocation;