-
-
Notifications
You must be signed in to change notification settings - Fork 10.4k
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
Closed
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
3e947b0
Test harness updates
brophdawg11 f184d48
Initial WIP implementation of beforeRequest
brophdawg11 f4ee6b7
Switch to middleware/next API
brophdawg11 fad256d
Middleware enhancements and tests
brophdawg11 ae498d8
middleware done, context typess needed
brophdawg11 890066f
fix depth and add context typings
brophdawg11 0a56a10
Add future flag
brophdawg11 545a8b7
Bump bundle
brophdawg11 9c41325
Add changeset and re-exports
brophdawg11 5e83ef5
bump
brophdawg11 414c4fb
Update typings
brophdawg11 4b89566
Leverage context param when flag is set
brophdawg11 42cc7c3
dont move things around for no reason
brophdawg11 d9b143b
Fix typo
brophdawg11 4d22813
Make requestContext optional and fix get/set generics
brophdawg11 e52c7e0
Fix (aka ignore) TS error
brophdawg11 1e51ffb
allow pre-populated context for static handler
brophdawg11 efd2e54
bundle bump
brophdawg11 0a853c5
Add future config to react-router create*Router methods
brophdawg11 d4ecfaf
Lift queryImpl 👋
brophdawg11 7ab2f19
Split submit/loadRouteData by query/queryRoute
brophdawg11 0b0efe5
Dedup query middlewares with render function
brophdawg11 14c7d4e
Switch to queryAndRender API
brophdawg11 3f9c740
Update createRoutesFromChildren snapshots to reflect middleware
brophdawg11 ff38e9e
Export UNSAFE_ stuff for use by remix server adaptors
brophdawg11 44b8496
Bump bundle
brophdawg11 4fe7b21
Update chnageset
brophdawg11 1cbab31
First stab at middleware docs
brophdawg11 0b233aa
Remove section from changeset on deduping since it's in docs now
brophdawg11 1e8a428
Add short circuiting tests
brophdawg11 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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