-
-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Add support for client context and middleware (unstable) #12941
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
Conversation
🦋 Changeset detectedLatest commit: 979cf6c The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
dataStrategy: ssrInfo.context.isSpaMode | ||
? undefined | ||
: getSingleFetchDataStrategy( | ||
ssrInfo.manifest, | ||
ssrInfo.routeModules, | ||
() => router | ||
), |
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.
SPA Mode never should have de-optimized revalidation the way single fetch does because there is no server
return runMiddlewarePipeline( | ||
args, | ||
matches.findIndex((m) => m.shouldLoad), | ||
false, | ||
async (keyedResults) => { | ||
let results = await singleFetchActionStrategy(request, matches); | ||
Object.assign(keyedResults, results); | ||
}, | ||
middlewareErrorHandler | ||
) as Promise<Record<string, DataStrategyResult>>; |
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.
Because middleware
is part of the default data strategy, we have to re-implement it here in our custom data strategy and can do so using the same runMiddlewarePipeline
API we use internally. I'm thinking we should make some form of this public API as well for userland dataStrategy
implementation who want to use the normal middleware.
The current API is as follows - may be leaking some implementation details we could hide in the exported version though:
runMiddlewarePipeline(
// Passthrough of { request, matches, context } from dataStrategy
args,
// how deep? I.e., what is the lowest handler to run
matchIndexToRunMiddlewareTo,
// Should I bubble up a returned Response? SSR only - always `false` in user client-side implementations
false,
// callback to run the handlers and assign results to keyedResults
// async (keyedResults: Record<string, DataStrategyResult>) { ... },
// Error callback if a middleware throws an error - assign the error to keyedResults
async (e: MiddlewareError, keyedResults: Record<string, DataStrategyResult>) { ... }
)
Maybe we could pass it as an arg to dataStrategy
? We could remove the boolean and handle that for them internally, and then instead of using an index we could just let them hand us the matches which they could .slice
if they didn't want to run all the way down:
function dataStrategy({ request, params, context, matches, runMiddleware }) {
return runMiddleware(
{ request, params, context },
matches,
(results) => { /* run handlers, assign to results */ },
(e, results) => { /* handle error */ },
);
})
return singleFetchLoaderNavigationStrategy( | ||
|
||
// Determine how deep to run middleware | ||
let lowestLoadingIndex = getLowestLoadingIndex( |
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.
We only run client middleware down to the lowest server loader
we will run
filterMatchesToLoad?: (match: AgnosticDataRouteMatch) => boolean; | ||
skipLoaderErrorBubbling?: boolean; | ||
dataStrategy?: DataStrategyFunction; | ||
skipRevalidation?: boolean; | ||
dataStrategy?: DataStrategyFunction<unknown>; |
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.
We used to use a dataStrategy
on the server to filter out matches (for ?_routes
support ) and to skip running loaders (for POST /path.data
requests) but it was one more layer of abstraction and they felt like useful APIs to have built in anyway so filterMatchesToLoad
/skipRevalidation
bring that logic into .query
so we can get rid of our server side dataStrategy
entirely
unstable_respond?: ( | ||
staticContext: StaticHandlerContext | ||
) => Response | Promise<Response>; |
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.
This is the new API on the static handler to enable middleware. Currently we return a StaticHandlerContext
from this method which is of the shape { loaderData, actionData, errors, ... }
. But for middleware we need to generate a Response
after running the loaders/actions that we can return from next
and bubble back up the middleware chain. So we had 2 options - we could implement server side middleware in server-runtime code entirely separate from the staticHandler but that feels inconsistent with the client side implementation and also potentially means a separate set of code.
Instead, if we have the user tell us how to convert StaticHandlerContext -> Response
then we can run all the middleware inside .query()
and use all the same code and make .query
a more useful API on it's own.
@@ -1581,6 +1597,9 @@ export function createRouter(init: RouterInit): Router { | |||
pendingNavigationController.signal, | |||
opts && opts.submission | |||
); | |||
// Create a new context per navigation that has references to all global | |||
// contextual fields | |||
let scopedContext = { ...unstable_RouterContext }; |
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.
Create a new context instance per navigation
/fetcher
call
return respond ? respond(staticContext) : staticContext; | ||
} | ||
|
||
if (respond && matches.some((m) => m.route.unstable_middleware)) { |
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.
If we received an unstable_respond
method and one of our routes has middleware, run the new code path. This means that we won't run the new code for anyone not opted into middleware
@@ -3681,6 +3900,48 @@ export function createStaticHandler( | |||
}; | |||
} | |||
|
|||
if (skipRevalidation) { |
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.
new skipRevalidation
behavior - short circuits after running the action
if (!args.matches.some((m) => m.route.unstable_middleware)) { | ||
return defaultDataStrategy(args); | ||
} |
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.
Run the old code if no middleware exists
propagateResult: boolean; | ||
}; | ||
|
||
export async function runMiddlewarePipeline( |
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.
This is the main implementation to run the middlewares and provide the next
method
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.
I made a few comments to try and line up types between this work and the work we'll be launching later this year in Remix. It would be nice if we shared the same middleware API between RR and Remix.
Co-authored-by: Mark Dalgleish <mark.john.dalgleish@gmail.com> Co-authored-by: Steven Liao <114700648+steven-liao-rs@users.noreply.github.com>
5fe5c3f
to
4ffbf90
Compare
decisions/0014-context-middleware.md
Outdated
|
||
We originally considered leaning on our existing `context` value we pass to server-side `loader` and `action` functions, and implementing a similar client-side equivalent for parity. However, the type story around `AppLoadContext` isn't great, so that would mean implementing a new API client side that we knew we weren't happy with from day one. And then likely replacing it with a better version fairly soon after. | ||
|
||
Instead, when the flag is enabled, we'll be removing `AppLoadContext` in favor of a type-safe `context` API that is similar in usage to the `React.createContext` API: |
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.
This will be a breaking change for many apps, is there no way to keep the AppLoadContext? I know many use cases can be solved by middlewares but being able to inject from the HTTP server into the RR app is useful
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.
From my initial testing it seems like it'll still be possible - you'll just need to pass/retrieve it a bit differently 😃
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.
Correct - you can use getLoadContext
to pass values through from the adapter - it just needs to conform to the new ContextProvider
API and return a Map<RouterContext, unknown>
: https://github.com/remix-run/react-router/blob/brophdawg11/feat-middleware/playground/middleware/server.ts#L26
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.
ohh in that case there's no issue, I would worry that this is a breaking change tho so should this be marked as v8_middleware once stable?
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.
Correct - you can use
getLoadContext
to pass values through from the adapter - it just needs to conform to the newContextProvider
API and return aMap<RouterContext, unknown>
: brophdawg11/feat-middleware/playground/middleware/server.ts#L26
This is going to hurt. A lot. We have hundreds of routes with lots of complex loader
and action
functions, and thousands of tests, that rely heavily on the current AppLoadContext
implementation.
This is 100% a breaking change and should be a v8
flag IMO.
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.
However, the type story around
AppLoadContext
isn't great
Can we get some elaboration on this part? I've been super happy with the type story around AppLoadContext
, I'd be curious to see where my experience differs from others.
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.
Yeah - this is a known breaking change and the flag will stabilize as future.v8_middleware
once it's ready
I hope it's helpful here, few issues from making branch with the experimental version at my job project:
Also thanks a lot for working on that, it's really awesome to use and allowed me to simplify the flow a lot! |
Is it possible to modify a request, maybe modify next() to accept a new request object? here's some code demonstration the idea: // Clone the request using request.clone()
const clonedRequest = request.clone();
// do something to modify cloned request
...
// Pass the cloned request to next middleware/loader
let response = await next({ /// Allow next to take a clonedRequest so it can be modified
request: clonedRequest,
params,
context
}); |
2e6bf91
to
6b61caa
Compare
@pawelblaszczyk5 Thanks for the feedback! 1/2 - The 3 - For now I don't think we'll do a non- 4 - I don't think you should ever be directly accessing It's an interesting idea - probably not something we'll add for initial release but feel free to open up a Proposal Discussion outlining your concrete use cases and we can see if it's a popular ask from the community? |
what would you need to change in the request that you couldn't use context to pass to the route loader/action? |
Re 2 - I'm not sure if that's fixed with latest changes - the issue is that with Re 3 - My use case was implementing middleware like Re 4 - tbh I don't even recall what was my idea with using |
You can put a generic/route-agnostic middleware anywhere in code and then just import it to all the routes that need it. If you don't want to use // app/middleware/auth.ts
import type { Route } from "../+types/root";
export const authMiddleware: Route.unstable_MiddlewareFunction = async (...) => {...} I did remove the first generic in a4ed390 because middleware should always be used with the new context type, so it should be simplified to |
You could create a pathless layout that renders nothing. export async function unstable_middleware(args: Route.MiddlewareArgs) {
// code
}
export default function Component() {
return <Outlet />
} |
I don’t think that’d work for all cases - I have some “api/“ routes that all share the same middleware but some of them should also use auth middleware. And the “app/“ routes should all use auth middleware - I think I can’t structure them even with path less layout to make this work 😃 |
@brophdawg11 does middlewares apply to resource routes? Right now resource routes are never nested so there's no point of a middleware there, it would be simpler to just call a function inside the resource route loader/action as currently. |
One question i currently having about resource routes and middlewares : what if I have a whole /protected/ route layout that apply a middleware to check the auth state, and then I have a nested resource route /protected/some-actions. My question is the following: will the auth middleware from /protected will apply to /protected/some-actions? |
Based on how it works right now, it shouldn't, because the resource route doesn't have parent routes |
This isn't a limitation? Resource routes can be nested, and middleware will apply to them just like other nested routes: https://stackblitz.com/edit/github-8b5yasye |
…d-route-typegen * upstream/dev: (65 commits) Generate types for `virtual:react-router/server-build` (remix-run#13152) Add support for client context and middleware (unstable) (remix-run#12941) Add playground for `vite-plugin-cloudflare` (remix-run#13151) do not typegen params for layout routes with a corresponding index (remix-run#13131) (remix-run#13140) Fix types for `loaderData` and `actionData` that contain `Record`s (remix-run#13139) chore: format chore(dev): remove unused dependencies (remix-run#13134) Remove unused Vite file system watcher (remix-run#13133) Remove stale changesets cherry-picked into release-next for 7.2.0 Fix custom SSR build input with `serverBundles` (remix-run#13107) Skip resource route flow in dev mode when SPA mode is enabled (remix-run#13113) chore: format Add integration test for `vite-plugin-cloudflare` (remix-run#13099) Fix custom client `build.rollupOptions.output.entryFileNames` (remix-run#13098) Detect lazy route discovery manifest version mismatches and trigger reloads (remix-run#13061) Fix critical CSS with custom `Vite.DevEnvironment` (remix-run#13066) Fix usage of `prerender` option with `serverBundles` (remix-run#13082) Fix support for custom `build.assetsDir` (remix-run#13077) Add changeset for remix-run#13064 Only import the root route when SSRing SPA mode's index.html (remix-run#13023) ...
🤖 Hello there, We just published version Thanks! |
Add middleware support to React Router behind a
future.unstable_middleware
flag. This also required a client-sidecontext
API forloader
/action
functions.See the changeset for example usage.
Middleware RFC: remix-run/remix#7642
Client Context RFC: #9856
Closes: #12695
Experimental release:
0.0.0-experimental-23ff92a9d