Skip to content
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

Add route middleware functionality #9975

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3e947b0
Test harness updates
brophdawg11 Jan 20, 2023
f184d48
Initial WIP implementation of beforeRequest
brophdawg11 Jan 20, 2023
f4ee6b7
Switch to middleware/next API
brophdawg11 Jan 25, 2023
fad256d
Middleware enhancements and tests
brophdawg11 Jan 26, 2023
ae498d8
middleware done, context typess needed
brophdawg11 Jan 26, 2023
890066f
fix depth and add context typings
brophdawg11 Jan 26, 2023
0a56a10
Add future flag
brophdawg11 Jan 27, 2023
545a8b7
Bump bundle
brophdawg11 Jan 27, 2023
9c41325
Add changeset and re-exports
brophdawg11 Jan 27, 2023
5e83ef5
bump
brophdawg11 Jan 27, 2023
414c4fb
Update typings
brophdawg11 Jan 27, 2023
4b89566
Leverage context param when flag is set
brophdawg11 Jan 27, 2023
42cc7c3
dont move things around for no reason
brophdawg11 Jan 27, 2023
d9b143b
Fix typo
brophdawg11 Jan 30, 2023
4d22813
Make requestContext optional and fix get/set generics
brophdawg11 Jan 31, 2023
e52c7e0
Fix (aka ignore) TS error
brophdawg11 Jan 31, 2023
1e51ffb
allow pre-populated context for static handler
brophdawg11 Feb 1, 2023
efd2e54
bundle bump
brophdawg11 Feb 2, 2023
0a853c5
Add future config to react-router create*Router methods
brophdawg11 Feb 6, 2023
d4ecfaf
Lift queryImpl 👋
brophdawg11 Feb 8, 2023
7ab2f19
Split submit/loadRouteData by query/queryRoute
brophdawg11 Feb 8, 2023
0b0efe5
Dedup query middlewares with render function
brophdawg11 Feb 8, 2023
14c7d4e
Switch to queryAndRender API
brophdawg11 Feb 8, 2023
3f9c740
Update createRoutesFromChildren snapshots to reflect middleware
brophdawg11 Feb 8, 2023
ff38e9e
Export UNSAFE_ stuff for use by remix server adaptors
brophdawg11 Feb 9, 2023
44b8496
Bump bundle
brophdawg11 Feb 9, 2023
4fe7b21
Update chnageset
brophdawg11 Feb 10, 2023
1cbab31
First stab at middleware docs
brophdawg11 Feb 15, 2023
0b233aa
Remove section from changeset on deduping since it's in docs now
brophdawg11 Feb 15, 2023
1e8a428
Add short circuiting tests
brophdawg11 Feb 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .changeset/hip-geckos-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
"@remix-run/router": minor
---

**Introducing Route Middleware**

**Proposal**: #9564

Adds support for middleware on routes to give you a common place to run before and after your loaders and actions in a single location higher up in the routing tree. The API we landed on is inspired by the middleware API in [Fresh](https://fresh.deno.dev/docs/concepts/middleware) since it supports the concept of nested routes and also allows you to run logic on the response _after_ the fact.

This feature is behind a `future.unstable_middleware` flag at the moment, but major API changes are not expected and we believe it's ready for production usage. This flag allows us to make small "breaking" changes if users run into unforeseen issues.

To opt into the middleware feature, you pass the flag to your `createBrowserRouter` (or equivalent) method, and then you can define a `middleware` function on your routes:

```tsx
import {
createBrowserRouter,
createMiddlewareContext,
RouterProvider,
} from "react-router-dom";
import { getSession, commitSession } from "../session";
import { getPosts } from "../posts";

// 👉 Create strongly-typed contexts to use as keys for your middleware data
let userCtx = createMiddlewareContext<User>(null);
let sessionCtx = createMiddlewareContext<Session>(null);

const routes = [
{
path: "/",
// 👉 Define middleware on your routes
middleware: rootMiddleware,
children: [
{
path: "path",
loader: childLoader,
},
],
},
];

const router = createBrowserRouter(routes, {
future: {
// 👉 Enable middleware for your router instance
unstable_middleware: true,
},
});

function App() {
return <RouterProvider router={router} />;
}

// Middlewares receive a context object with get/set/next methods
async function rootMiddleware({ request, context }) {
// 🔥 Load common information in one spot in your middleware and make it
// available to child middleware/loaders/actions
let session = await getSession(request.headers.get("Cookie"));
let user = await getUser(session);
context.set(userCtx, user);
context.set(sessionCtx, session);

// Call child middleware/loaders/actions
let response = await context.next();

// 🔥 Assign common response headers on the way out
response.headers.append("Set-Cookie", await commitSession(session));
return response;
}

async function childLoader({ context }) {
// 🔥 Read strongly-typed data from ancestor middlewares
let session = context.get(sessionCtx);
let user = context.get(userCtx);

let posts = await getPosts({ author: user.id });
return redirect(`/posts/${post.id}`);
}
```
107 changes: 107 additions & 0 deletions docs/route/middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: middleware
new: true
---

# `middleware`

React Router tries to avoid network waterfalls by running loaders in parallel. This can cause some code duplication for logic required across multiple loaders (or actions) such as validating a user session. Middleware is designed to give you a single location to put this type of logic that is shared amongst many loaders/actions. Middleware can be defined on any route and is run _sequentially_ top-down _before_ a loader/action call and then bottom-up _after_ the call.

Because they are run sequentially, you can easily introduce inadvertent network waterfalls and slow down your page loads and route transitions. Please use carefully!

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

<docs-warning>This feature is currently enabled via a `future.unstable_middleware` flag passed to `createBrowserRouter`</docs-warning>

```tsx [2,6-26,32]
// Context allows strong types on middleware-provided values
let userContext = createMiddlewareContext<User>();

<Route
path="account"
middleware={async ({ request, context }) => {
let user = getUser(request);

// Provide user object to all child routes
context.set(userContext, user);

// Continue the Remix request chain running all child middlewares sequentially,
// followed by all matched loaders in parallel. The response from the underlying
// loader is then bubbles back up the middleware chain via the return value.
let response = await context.next();

// Set common outgoing headers on all responses
response.headers.set("X-Custom", "Stuff");

// Return the altered response
return response;
}}
>
<Route
path="profile"
loader={async ({ context }) => {
// Guaranteed to have a user if this loader runs!
let user = context.get(userContext);
let data = await getProfile(user);
return json(data);
}}
/>
</Route>;
```

## Arguments

Middleware receives the same arguments as a `loader`/`action` (`request` and `params`) as well as an additional `context` parameter. `context` behaves a bit like [React Context][react-context] in that you create a context which is the strongly-typed to the value you provide.

```tsx
let userContext = createMiddlewareContext<User>();
// context.get(userContext) => Return type is User
// context.set(userContext, user) => Requires `user` be of type User
```

When middleware is enabled, this `context` object is also made available to actions and loaders to retrieve values set by middlewares.

## Logic Flow

Middleware is designed to solve 4 separate use-cases with a single API to keep the API surface compact:

- I want to run some logic before a loader
- I want to run some logic after a loader
- I want to run some logic before an action
- I want to run some logic after an action

To support this we adopted an API inspired by the middleware implementation in [Fresh][fresh-middleware] where the function gives you control over the invocation of child logic, thus allowing you to run logic both before and after the child logic. You can differentiate between loaders and actions based on the `request.method`.

```tsx
async function middleware({ request, context }) {
// Run me top-down before action/loaders run
// Provide values to descendant middleware + action/loaders
context.set(userContext, getUser(request));

// Run descendant middlewares sequentially, followed by the action/loaders
let response = await context.next();

// Run me bottom-up after action/loaders run
response.headers.set("X-Custom", "Stuff");

// Return the response to ancestor middlewares
return response;
}
```

Because middleware has access to the incoming `Request` _and also_ has the ability to mutate the outgoing `Response`, it's important to note that middlewares are executed _per-unique Request/Response combination_.

In client-side React Router applications, this means that nested routes will execute middlewares _for each loader_ because each loader returns a unique `Response` that could be altered independently by the middleware.

When navigating to `/a/b`, the following represents the parallel data loading chains:

```
a middleware -> a loader -> a middleware
a middleware -> b middleware -> b loader -> b middleware -> a middleware
```

So you should be aware that while middleware will reduce some code duplication across your actions/loaders, you may need to leverage a mechanism to dedup external API calls made from within a middleware.

[pickingarouter]: ../routers/picking-a-router
[react-context]: https://reactjs.org/docs/context.html
[fresh-middleware]: https://fresh.deno.dev/docs/concepts/middleware
Copy link

@cliffordfajardo cliffordfajardo Feb 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you would middleware composition look like with this new API?
For example, what If I needed to apply 3-4 middleware to my request, which is not an uncommon thing if you're coming from Express or Fastify or in general a production nodejs server 😄

Recently stumbled upon this lib which takes a fetch API first approach & feels very vanilla, thought i'd share 👀
https://github.com/data-eden/data-eden/tree/main/packages/network

CleanShot 2023-02-28 at 04 01 19@2x

51 changes: 51 additions & 0 deletions docs/route/route.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,56 @@ The route action is called when a submission is sent to the route from a [Form][

Please see the [action][action] documentation for more details.

## `middleware`

React Router tries to avoid network waterfalls by running loaders in parallel. This can cause some code duplication for logic required across multiple loaders (or actions) such as validating a user session. Middleware is designed to give you a single location to put this type of logic that is shared amongst many loaders/actions. Middleware can be defined on any route and is run _both_ top-down _before_ a loader/action call and then bottom-up _after_ the call.

For example:

```tsx [2,6-26,32]
// Context allow strong types on middleware-provided values
let userContext = createMiddlewareContext<User>();

<Route
path="account"
middleware={async ({ request, context }) => {
let user = getUser(request);
if (!user) {
// Require login for all child routes of /account
throw redirect("/login");
}

// Provide user object to all child routes
context.set(userContext, user);

// Continue the Remix request chain running all child middlewares sequentially,
// followed by all matched loaders in parallel. The response from the underlying
// loader is then bubbles back up the middleware chain via the return value.
let response = await context.next();

// Set common outgoing headers on all responses
response.headers.set("X-Custom", "Stuff");

// Return the altered response
return response;
}}
>
<Route
path="profile"
loader={async ({ context }) => {
// Guaranteed to have a user if this loader runs!
let user = context.get(userContext);
let data = await getProfile(user);
return json(data);
}}
/>
</Route>;
```

<docs-warning>If you are not using a data router like [`createBrowserRouter`][createbrowserrouter], this will do nothing</docs-warning>

Please see the [middleware][middleware] documentation for more details.

## `element`

The element to render when the route matches the URL.
Expand Down Expand Up @@ -334,6 +384,7 @@ Any application-specific data. Please see the [useMatches][usematches] documenta
[useloaderdata]: ../hooks/use-loader-data
[loader]: ./loader
[action]: ./action
[middleware]: ./middleware
[errorelement]: ./error-element
[form]: ../components/form
[fetcher]: ../hooks/use-fetcher
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
},
"filesize": {
"packages/router/dist/router.umd.min.js": {
"none": "41.5 kB"
"none": "44.6 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "13 kB"
Expand All @@ -114,7 +114,7 @@
"none": "15 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "11.5 kB"
"none": "11.6 kB"
},
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
"none": "17.5 kB"
Expand Down
14 changes: 14 additions & 0 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
Fetcher,
FormEncType,
FormMethod,
FutureConfig,
GetScrollRestorationKeyFunction,
HashHistory,
History,
Expand Down Expand Up @@ -76,21 +77,29 @@ export { createSearchParams };
export type {
ActionFunction,
ActionFunctionArgs,
ActionFunctionWithMiddleware,
ActionFunctionArgsWithMiddleware,
AwaitProps,
unstable_Blocker,
unstable_BlockerFunction,
DataRouteMatch,
DataRouteObject,
Fetcher,
FutureConfig,
Hash,
IndexRouteObject,
IndexRouteProps,
JsonFunction,
LayoutRouteProps,
LoaderFunction,
LoaderFunctionArgs,
LoaderFunctionWithMiddleware,
LoaderFunctionArgsWithMiddleware,
Location,
MemoryRouterProps,
MiddlewareContext,
MiddlewareFunction,
MiddlewareFunctionArgs,
NavigateFunction,
NavigateOptions,
NavigateProps,
Expand Down Expand Up @@ -129,6 +138,7 @@ export {
RouterProvider,
Routes,
createMemoryRouter,
createMiddlewareContext,
createPath,
createRoutesFromChildren,
createRoutesFromElements,
Expand Down Expand Up @@ -201,12 +211,14 @@ export function createBrowserRouter(
routes: RouteObject[],
opts?: {
basename?: string;
future?: Partial<FutureConfig>;
hydrationData?: HydrationState;
window?: Window;
}
): RemixRouter {
return createRouter({
basename: opts?.basename,
future: opts?.future,
history: createBrowserHistory({ window: opts?.window }),
hydrationData: opts?.hydrationData || parseHydrationData(),
routes: enhanceManualRouteObjects(routes),
Expand All @@ -217,12 +229,14 @@ export function createHashRouter(
routes: RouteObject[],
opts?: {
basename?: string;
future?: Partial<FutureConfig>;
hydrationData?: HydrationState;
window?: Window;
}
): RemixRouter {
return createRouter({
basename: opts?.basename,
future: opts?.future,
history: createHashHistory({ window: opts?.window }),
hydrationData: opts?.hydrationData || parseHydrationData(),
routes: enhanceManualRouteObjects(routes),
Expand Down
9 changes: 9 additions & 0 deletions packages/react-router-native/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,29 @@ import URLSearchParams from "@ungap/url-search-params";
export type {
ActionFunction,
ActionFunctionArgs,
ActionFunctionWithMiddleware,
ActionFunctionArgsWithMiddleware,
AwaitProps,
unstable_Blocker,
unstable_BlockerFunction,
DataRouteMatch,
DataRouteObject,
Fetcher,
FutureConfig,
Hash,
IndexRouteObject,
IndexRouteProps,
JsonFunction,
LayoutRouteProps,
LoaderFunction,
LoaderFunctionArgs,
LoaderFunctionWithMiddleware,
LoaderFunctionArgsWithMiddleware,
Location,
MemoryRouterProps,
MiddlewareContext,
MiddlewareFunction,
MiddlewareFunctionArgs,
NavigateFunction,
NavigateOptions,
NavigateProps,
Expand Down Expand Up @@ -75,6 +83,7 @@ export {
RouterProvider,
Routes,
createMemoryRouter,
createMiddlewareContext,
createPath,
createRoutesFromChildren,
createRoutesFromElements,
Expand Down
Loading