Minor Changes
-
BREAKING CHANGE: Simplified route action, controller, request handler, and middleware helper types.
Actionnow accepts a route pattern,RoutePattern, orRouteobject as its first generic and the full request context as its optional second generic. It describes either a plain request handler function or an action object with optional inline middleware.Controllernow accepts the route map as its first generic and the full request context as its optional second generic.RequestHandlernow accepts the full request context as its only generic.Middlewarenow accepts one context effect generic, which can be a single{ key, value }context entry, aContextEntriestuple, or a context transform function.BuildActionis no longer exported.For most apps, augment
RouterTypes.contextonce and usecreateAction()/createController()to type stored handlers withoutsatisfiesclauses:import { createAction, createController, type ContextWithParams, type RequestContext, } from 'remix/fetch-router' type AppContext<params extends Record<string, string> = {}> = ContextWithParams< RequestContext, params > declare module 'remix/fetch-router' { interface RouterTypes { context: AppContext } } let action = createAction(routes.account, { handler(context) { return new Response(context.params.id) }, }) let controller = createController(routes, { actions: { account(context) { return new Response(context.params.id) }, }, })
If you manually type actions or controllers in advanced multi-router code, compose the full context type first and pass it as the second generic:
import type { Action, MiddlewareContext } from 'remix/fetch-router' let accountMiddleware = [requireAuth<AuthIdentity>()] as const type AccountContext = MiddlewareContext<typeof accountMiddleware, AppContext> let action: Action<typeof routes.account, AccountContext> = { middleware: accountMiddleware, handler(context) { let auth = context.get(Auth) return Response.json(auth.identity) }, }
Actioncan be used to manually annotate either action form:let handler: Action<typeof routes.account, AccountContext> = (context) => { return Response.json(context.get(Auth).identity) } let action: Action<typeof routes.account, AccountContext> = { middleware: accountMiddleware, handler(context) { return Response.json(context.get(Auth).identity) }, }
Renamed the custom router matcher payload type from
MatchDatatoRouteEntry:// before let matcher = createMatcher<MatchData>() // after let matcher = createMatcher<RouteEntry>()
If you manually annotate request handlers, pass the full request context type as the only generic:
// before let handler: RequestHandler<{ id: string }, RequestContext<{ id: string }>> // after let handler: RequestHandler<RequestContext<{ id: string }>>
If you manually annotate middleware, pass only the context transform type:
// before let middleware: Middleware<{}, SetDatabaseContextTransform> // after let middleware: Middleware<{ key: typeof Database; value: Database }>
Simplified the public middleware context helper types.
MiddlewareContextis now the exported helper for deriving the request context produced by a middleware chain, and it accepts an optional base context as its second type parameter. Middleware that provides one context value can use a{ key, value }entry directly instead of wrapping the entry in a one-item tuple. The lower-levelMiddlewareContextTransform,ContextTransform,ApplyContextTransform,ApplyMiddleware, andApplyMiddlewareTuplehelpers are no longer exported.MiddlewareContextaccepts middleware values, not middleware factory function types. UseReturnType<typeof factory>when a middleware is created by a factory function:// before type AppContext = MiddlewareContext<[typeof session]> // after type AppContext = MiddlewareContext<[ReturnType<typeof session>]>
// before type AppContext = ApplyMiddlewareTuple<RequestContext, typeof middleware> // after type AppContext = MiddlewareContext<typeof middleware>
// before type ActionContext = ApplyMiddlewareTuple<AppContext, typeof actionMiddleware> // after type ActionContext = MiddlewareContext<typeof actionMiddleware, AppContext>
Renamed request context helper types so their names describe the
RequestContexttype they produce. UseContextWithParamswhen deriving an app context that includes route params:// before type AppContext<params extends AnyParams = {}> = WithParams< MiddlewareContext<typeof middleware>, params > // after type AppContext<params extends AnyParams = {}> = ContextWithParams< MiddlewareContext<typeof middleware>, params >
Use
ContextWithEntrieswhen manually composing one or more context entries without a middleware tuple:// before type CurrentUserContext = MergeContext<AppContext, [readonly [typeof CurrentUser, User | null]]> // after type CurrentUserContext = ContextWithEntries< AppContext, [{ key: typeof CurrentUser; value: User | null }] >
Use
ContextWithEntrywhen refining a single context entry for a specific handler or middleware result:// before type AdminContext = SetContextValue<AppContext, typeof CurrentRole, 'admin'> // after type AdminContext = ContextWithEntry<AppContext, { key: typeof CurrentRole; value: 'admin' }>
Stored action objects and controllers no longer derive handler context from their local middleware tuple. If local middleware adds context values that a handler requires, compose the full handler context explicitly and pass it to
Action,Controller,createAction(), orcreateController():// before let controller = createController(routes, { middleware: [requireAuth<AuthIdentity>()], actions: { account(context) { let auth = context.get(Auth) return Response.json(auth.identity) }, }, }) // after let accountMiddleware = [requireAuth<AuthIdentity>()] as const type AuthenticatedAppContext = MiddlewareContext<typeof accountMiddleware, AppContext> let controller = createController<typeof routes, AuthenticatedAppContext>(routes, { middleware: accountMiddleware, actions: { account(context) { let auth = context.get(Auth) return Response.json(auth.identity) }, }, })
-
BREAKING CHANGE:
context.get(key)now returnsundefinedwhen the requested value is not available in request context and the key does not provide a default value. Constructor keys such asFormDataandSessionstill infer their instance value type when they are set, but an emptyRequestContextno longer typescontext.get(FormData)as available by default.If your code reads a constructor key from a broad
RequestContext, handle the missing case before using the value:// before function readName(context: RequestContext): string { return String(context.get(FormData).get('name') ?? '') } // after function readName(context: RequestContext): string { return String(context.get(FormData)?.get('name') ?? '') }
Handlers whose context contract proves that middleware provides the key can keep reading a defined value. Keep middleware arrays tuple-typed when you want that context contribution to flow into handlers:
let router = createRouter({ middleware: [formData()] as const, }) router.post('/profile', (context) => { let formData = context.get(FormData) return Response.json({ name: formData.get('name') }) })
-
BREAKING CHANGE:
router.map(routes, controller)now maps a controller to the direct leaf routes in the route map passed torouter.map(). Controller actions may only include direct route leaves from that route map. Nested route-map keys must be mapped with a separaterouter.map()call, unknown action keys throw at runtime, and direct leaf routes still require matching actions.If an app currently maps nested route maps in one
router.map()call:router.map(routes, { middleware: [requireAuth()], actions: { home, account: { actions: { settings, }, }, }, })
Map each route map to its own controller:
router.map(routes, { actions: { home }, }) router.map(routes.account, { middleware: [requireAuth()], actions: { settings }, })
Controller middleware only runs for the direct actions in that controller; it is not inherited by controllers registered for nested route maps. Move shared protection such as
requireAuth()orrequireAdmin()onto every controller that needs it, or keep truly global behavior in the router-level middleware stack. -
BREAKING CHANGE:
router.fetch()no longer clonesRequestinputs or supportsRequestfacades that only become native requests throughclone(). Pass a realRequestobject torouter.fetch()when dispatching an existing request. -
BREAKING CHANGE:
RequestContext.methodis now typed asstringinstead ofRequestMethod, matching the Fetch API and allowing custom or extension request methods. Use the newisRequestMethod()helper to narrow a string to one of the router-supportedRequestMethodvalues when needed. -
Added support for middleware-installed direct request context properties. Middleware can now declare a context entry as
{ key, value, property: 'name' }and callcontext.set(key, value, { property: 'name' })to make the value available ascontext.namein handlers while preservingcontext.get(key)access. The context key remains the source of truth; direct properties are installed as non-enumerable getters that read from keyed request context.Direct property names are checked at runtime. Empty property names are rejected, a single context key cannot be installed under multiple property names, different context keys cannot share the same property name, and middleware cannot install a property that already exists on
RequestContext.The new
ContextEntrytype describes object-shaped middleware entries. Use{ key: typeof Database, value: Database, property: 'db' }for middleware that should exposecontext.db, or omitpropertywhen the value should only be read withcontext.get(key).When the same key/property pair is declared more than once, the last declaration determines the property type.
Patch Changes
- Bumped
@remix-run/*dependencies: