decisions/0007-remix-on-react-router-6-4-0.md
Date: 2022-08-16
Status: accepted
Now that we're almost done Remixing React Router and will be shipping [email protected] shortly, it's time for us to start thinking about how we can layer Remix on top of the latest React Router. This will allow us to delete a bunch of code from Remix for handling the Data APIs. This document aims to discuss the changes we foresee making and some potential iterative implementation approaches to avoid a big-bang merge.
From an iterative-release viewpoint, there's 4 separate "functional" aspects to consider here:
(1) can be implemented and deployed in isolation. (2) and (3) need to happen together since the contexts/components need to match. And (4) comes for free since the loaders/actions will be included on the routes we create in (3).
The high level approach is as follows
handleResourceRequest to use createStaticHandler behind a flag
handleDataRequest in the same mannerhandleDocumentRequest in the same manner
RemixContext data into EntryContext and remove old flow@remix-run/server-runtime changes once comfortable@remix-run/react in a short-lived feature branch
EntryContext with RemixContext)@remix-run/react changes once comfortableThere are 2 main areas where we have to make changes:
@remix-run/server-runtime (mainly in the server.ts file)@remix-run/react (mainly in the components.ts, server.ts and browser.ts files)Since these are separated by the network chasm, we can actually implement these independent of one another for smaller merges, iterative development, and easier rollbacks should something go wrong.
There's two primary reasons it makes sense to handle the server-side data-fetching logic first:
createStaticHandlerWe can do this on the server using the strangler pattern so that we can confirm the new approach is functionally equivalent to the old approach. Depending on how far we take it, we can assert this through unit tests, integration tests, as well as run-time feature flags if desired.
For example, pseudo code for this might look like the following, where we enable via a flag during local development and potentially unit/integration tests. We can throw exceptions anytime the new static handler results in different SSR data. Once we're confident, we delete the current code and remove the flag conditional.
// Runtime-agnostic flag to enable behavior, will always be committed as
// `false` initially, and toggled to true during local dev
const ENABLE_REMIX_ROUTER = false;
async function handleDocumentRequest({ request }) {
const appState = {
trackBoundaries: true,
trackCatchBoundaries: true,
catchBoundaryRouteId: null,
renderBoundaryRouteId: null,
loaderBoundaryRouteId: null,
error: undefined,
catch: undefined,
};
// ... do all the current stuff
const serverHandoff = {
actionData,
appState: appState,
matches: entryMatches,
routeData,
};
const entryContext = {
...serverHandoff,
manifest: build.assets,
routeModules,
serverHandoffString: createServerHandoffString(serverHandoff),
};
// If the flag is enabled, process the request again with the new static
// handler and confirm we get the same data on the other side
if (ENABLE_REMIX_ROUTER) {
const staticHandler = unstable_createStaticHandler(routes);
const context = await staticHandler.query(request);
// Note: == only used for brevity ;)
assert(entryContext.matches === context.matches);
assert(entryContext.routeData === context.loaderData);
assert(entryContext.actionData === context.actionData);
if (catchBoundaryRouteId) {
assert(appState.catch === context.errors[catchBoundaryRouteId]);
}
if (loaderBoundaryRouteId) {
assert(appState.error === context.errors[loaderBoundaryRouteId]);
}
}
}
We can also split this into iterative approaches on the server too, and do handleResourceRequest, handleDataRequest, and handleDocumentRequest independently (either just implementation or implementation + release). Doing them in that order would also likely go from least to most complex.
process.env since the code we're changing is runtime agnostic. We'll go with a local hardcoded variable in server.ts for now to avoid runtime-specific ENV variable concerns.
remixContext sent through entry.server.ts will be altered in shape. We consider this an opaque API so not a breaking change.createHierarchicalRoutes to build RR DataRouteObject instances
createStaticHandlerDataRoutes in the brophdawg11/rrr branchunstable_createStaticHandlerhandleResourceRequest
Response from queryRoutehandleDataRequest
handleDocumentRequest
/a/b/c, if C exports a CatchBoundary but not an ErrorBoundary, then it'll be represented in the DataRouteObject with hasErrorBoundary=true since the @remix-run/router doesn't distinguisherrorElement, but we then need to re-bubble that upwards to the nearest ErrorBoundarydifferentiateCatchVersusErrorBoundaries in the brophdawg11/rrr branchRemixContext
manifest, routeModules, staticHandlerContext, serverHandoffStringEntryContext assert the values matchstaticHandlerContext and can use getStaticContextFromError to get a new context for the second pass (note the need to re-call differentiateCatchVersusErrorBoundaries)The rendering layer in @remix-run/react is a bit more of a whole-sale replacement and comes with backwards-compatibility concerns, so it makes sense to do second. However, we can still do this iteratively, we just can't deploy iteratively since the SSR and client HTML need to stay synced (and associated hooks need to read from the same contexts). First, we can focus on getting the SSR document rendered properly without <Scripts/>. Then second we'll add in client-side hydration.
The main changes here include:
RemixEntry and it's context in favor of a new RemixContext.Provider wrapping DataStaticRouter/DataBrowserRouter
manifest, routeModules)staticHandlerContext during SSR)@remix-run/react's components.tsx file are now fully redundant and can be removed completely in favor of re-exporting from react-router-dom:
Form, useFormAction, useSubmit, useMatches, useFetchersLink, useLoaderData, useActionData, useTransition, useFetcheruseLoaderData/useActionData need to retain their generics, and are not currently generic in react-routeruseTransition needs submission and type added
<Form method="get"> no longer goes into a "submitting" state in react-router-domuseFetcher needs type addedunstable_shouldReload replaced by shouldRevalidate
shouldRevalidate?Request.signal - continue to send separate signal param