๐บ๏ธ Single Data Fetch #7640
Replies: 28 comments 84 replies
-
What happens in resource routes? They will still need to return responses, right? This will cause an inconsistency, unless there's a separate export (not a loader or action) for resource routes, like export async function handler({ request }: DataFunctionArgs) {
return new Response();
} That can work for any HTTP method. |
Beta Was this translation helpful? Give feedback.
-
Will this include a dataloader for shared data like we once talked about? If I remember correctly it was something kinda like this: export async function loader({ load }) {
let result = await load(async () => {
// do the fetch here
})
} |
Beta Was this translation helpful? Give feedback.
-
I'm not completely convinced of ditching the simple request -> response flow of loaders and actions. Is there no other way to achieve the same benefits without losing that? |
Beta Was this translation helpful? Give feedback.
-
The example of In this example, would each data function be responsible for the merging its self and actually need to use the headers.append("Server-Timing", `db;dur=${dbTime}`); |
Beta Was this translation helpful? Give feedback.
-
This is interesting, but I wonder how And what about testing and mocking (like using MSW)? Seems like some stuff will change with this |
Beta Was this translation helpful? Give feedback.
-
How will the opt-in granular revalidation work in detail? Can I still control (via a custom header) what loaders get revalidated? For example, will it still be possible to fetch data in a parent loader (used via "usedRouteLoaderData" in child routes) and prevent revalidation for this particular parent loader for normal document requests? |
Beta Was this translation helpful? Give feedback.
-
@ryanflorence would single data fetch solve the issues for things like flash messages being cleared out? e.g. I have a flash message session that is read at the |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
A couple points of concern around status/header handling:
const thing = await getThingBySlug(slug)
if (!thing || thing.isDeleted) {
throw notFound({ message: 'Thing not found' });
}
const actualSlug = thing.slug;
if (slug !== actualSlug) {
const url = new URL(request.url);
const redirectTo = url.pathname.replace(slug, actualSlug);
throw redirect(redirectTo);
}
return { thing } Would the proposal be to manually set Maybe a wrapper or v1-compat helps this situation where any thrown Response still works but just does the 2-3 steps of headers, status, and data return for you? |
Beta Was this translation helpful? Give feedback.
-
Two questions
|
Beta Was this translation helpful? Give feedback.
-
Why not send data request with |
Beta Was this translation helpful? Give feedback.
-
I'm a little concerned/confused about the "waterfall" of status checking. Given this deep tree of child routes
Am I understanding correctly that unless route I think this affects refactoring and code portability as well โ a child route shouldn't have to "know" about it's parent route's possible status values. I also don't want to have to stick this chunk of code in 100% of my route loaders. Then again, I could be misunderstanding the intention and/or the actual desired implementation, in which case you are free to ignore me ๐ |
Beta Was this translation helpful? Give feedback.
-
I feel this is abstracting a bit too much from Remix current behaviour and feels like a super breaking change that would make basically all past apps completely unusable. Still, I see the issues the current setup is creating to convert to RSC or to implement middlewares so I understand where this is coming from and I appreciate your careful considerations. What I am concerned, if a full breaking change is absolutely needed as you are well explaining in the RFC, is the API is a little hard to grasp. In my small experience, after trying a lot of frameworks, the best at actually handling server-side requests, is Express. In its simplicity it just does everything is needed, from middlewares to error handling and it lets you send any response you want. Its syntax is also globally appreciated by JS developers, every time you move to a JS meta-framework, it seems like you need to sacrifice a bit on the backend not using Express in its simplicity. I'm not sure how to implement this, but, if we need to completely revolutionise loaders, I feel going to a more Express-like syntax would be beneficial. i.e.: Adding the possibility of ending the request immediately and ditch any subsequent loaders in any of the loaders by returning res.status(201).json({status: "success", data: "user successfully created"}); Being able to call These are the parts I feel are missing the most in Remix right now and going back to Express and leverage is well estabilished and known syntax might be a nice way of making Remix more inviting for backend devs too. |
Beta Was this translation helpful? Give feedback.
-
Maybe there should be a way to opt-out of merging for a specific routes/loaders? This could be the same mechanism used in API routes as well. For example if the loader returns a |
Beta Was this translation helpful? Give feedback.
-
A few commerce use-cases to keep in mind:
|
Beta Was this translation helpful? Give feedback.
-
Let me check my understanding here. A Response is constructed with 4 things. Remix is proposing the following method to assemble a Response 1. body (see https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#body)The body will be returned from the loader. Will be of type 2. headersA 3. statusstatus is set using a 4. statusTextRemix is not proposing supporting at this time. |
Beta Was this translation helpful? Give feedback.
-
Our client side navigations are currently benefiting from having independent fetches because the data returned from each loader has different freshness requirements. For example, our ecom PDP page is constructed like this:
Where
This makes navigating from PLP to PDP etc. very quick because it only revalidates the changing route (3.) which is cacheable. In order to maintain this benefit, I'm guessing we'd a) need to implement the revalidation custom header proposed in this RFC and b) set a If it's not supported well enough, then perhaps Remix could also support accepting the value as a query param to the |
Beta Was this translation helpful? Give feedback.
-
Will this feature flag also be available in react router somehow? |
Beta Was this translation helpful? Give feedback.
-
Borrowing from ideas in This would allow you to do some pretty cool stuff if you're using (for example) a GraphQL API behind the scenes // posts.tsx
export function hybridLoader() {
return useGetList('posts', [ 'id', 'title' ]);
}
// posts/$id.tsx
export function hybridLoader({ params }) {
return useGetOne('posts', params.id, ['id', 'title', 'prose'])
} Behind the scenes, a data loader for a GraphQL API could translate that into the following example query: query example(id: ID!) {
posts {
id
title
}
post(id: $id) {
id
title
prose
}
} Now the client version of this can roll up multiple calls to these resource functions and send it in a single round trip.
|
Beta Was this translation helpful? Give feedback.
-
I see the benefits of this new approach, but I feel like we are moving away from the "use the platform" philosophy by creating those special My proposal would be the following (I would like a feedback if it makes sense):
export function loader({ responseInit }) {
let [stuff, dbTime] = myTimingFunc(() => myDb.stuff());
responseInit.headers.append("Server-Timing", `db;dur=${dbTime}`);
return stuff;
}
export function action({ request, responseInit }) {
let [errors, record] = await createThing(request);
if (errors) {
responseInit.status = 400
return errors;
}
return record;
}
const parentStatus = responseInit.status
if(parentStatus < 400) responseInit.status = 200
export function loader({ responseInit }) {
let stuff = await myDb.stuff();
if (!stuff) {
responseInit.status = 400
throw Response.json({ message: "Not Found" }, { status: responseInit.status });
}
return stuff;
} |
Beta Was this translation helpful? Give feedback.
-
How would this work with // root.tsx
export const clientLoader = async (args) => {
await sleep(10_000)
return await args.serverLoader()
}
// _index.tsx
export const clientLoader = async (args) => {
return await args.serverLoader()
} |
Beta Was this translation helpful? Give feedback.
-
I might be misunderstanding something, but does this mean that longpolling and/or Server-Sent Events will no longer be possible? Both cases are achieved only by directly controlling |
Beta Was this translation helpful? Give feedback.
-
O Lords of Remix: |
Beta Was this translation helpful? Give feedback.
-
Nice, managing session headers was troublesome. Hopefully it will also resolve socket hang up issue. |
Beta Was this translation helpful? Give feedback.
-
Will Remix be able to provide a utility like |
Beta Was this translation helpful? Give feedback.
-
Hi, folks ๐ I wanted to ask a quick question: what is the alternative to Intent: I want to send data from a fetch('/my-route?_data=routes/my-route')
With Single fetch, requests with But what is the alternative for this use case? Even if it's not something you want to publicly support, I'd love to know what are the options, if any. Is there no way to make a UI route behave as a resource route in some situations? I have a working version of this behavior on the stable version of Remix, now trying to support Single fetch. Thanks.
|
Beta Was this translation helpful? Give feedback.
-
Is there a known latency issue with single-fetch? I am trying to understand why my routes take +100ms to produce a response, despite loaders finishing all the work in under 30ms. ![]() In screenshot you see a trace of https://pillser.com/supplements/vitamin-b1-3254. The loaders complete under ~30ms, but the document_request does not complete for another 100ms. Is this related to |
Beta Was this translation helpful? Give feedback.
-
Return Headers from a
As of v2.11.0 this API has been removed per the changelog: https://github.com/remix-run/remix/blob/main/CHANGELOG.md#removed-response-stub-in-single-fetch-unstable It took me a little while to find this, but this is how you can return headers with the new API of import { unstable_data as data } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
return data({ hello: "standard data here" }, { headers: [["Set-Cookie", "cookie-value"]] });
} This |
Beta Was this translation helpful? Give feedback.
-
Single Fetch for Data
This proposal aims to simplify the data fetching story in Remix by making document and data requests consistent by making a single fetch for data instead of one per route.
While this has current benefits (like better CDN caching for pages) it also prepares apps for future features:
Benefits today
Future Benefits
Background
When a document request is made to Remix today, it calls all loaders in the same request, takes every response returned from loaders and attempts to merge them into a single response, and finally returns the document.
When the user navigates to another page with client side routing, Remix decides which route loaders to call an then makes a fetch from the browser for each loader in separate requests.
Problems
Today
In the future
As we explored middleware, we weren't satisfied with the behavior that would be required to support it with the current data fetching behavior. A single middleware would be called multiple times for the same client side navigation. This is unintuitive and inefficient.
Being able to anticipate a single request per navigation will make several other future features easier to implement and use. For example, Remix could provide ways to share data between loaders (and even middleware) so that code naturally avoids making the same calls to the data store twice even if the application code looks that way.
Proposal
Future Flag
Turning on a future flag will enable the new behavior:
New Behavior
.json
on the end and anX-Remix-Data
header.API Changes
Loaders and actions can no longer return Responses, but instead simply return data.
Data functions set headers with a new
headers
param passed to them.The
headers
export is no longer used and no application level merge is required anymore.Data functions receive a new
status
function to set the status of the response. The last route to call this wins.A function can be used to get access to the status any parent routes may have set:
Though loaders are still called in parallel, the status function will be deterministically called in order from parent to child.
Instead of throwing Responses, data functions throw a
new BadResponse()
. The values passed to the constructor will be available in the ErrorBoundary withuseLoaderData
.deprecate
useRouteError
, but it will continue to return the same thing asuseLoaderData
inside of an ErrorBoundary.deprecate
isRouteErrorResponse
, error boundary components can simply ask ifdata instanceof BadResponse
.Revalidation Behavior
Today's Behavior
Today the fetches Remix makes on navigations is determined by the changing routes and the
shouldRevalidate
exports of all routes.shouldRevalidate
, then the fetch is made if it returnstrue
, or it can opt out withfalse
shouldRevalidate
, then it has the potential to make a fetch ifshouldRevladate
returns true.Future Behavior
By default, All routes are revalidated on navigation, just like a normal document request. This is predictable, easy to understand, and consistent with the behavior of document requests. It also makes HTTP caching more efficient.
Opt-in granular revalidation through
shouldRevalidate
is done through headers. Remix will avoid calling loaders that are not present in a custom header. While some intermediate caches don't work well with headers, they are different optimizations with little (if any) practical overlap: if you are able to use CDN caching of an entire document, you won't need granular revalidation of individual loaders and visa versa.URLs end in
.json
instead of a_data
search param. This is easier for many intermediate caches to work with and also opens the door for static pre-fetching (Files can't have search params, but they can have.json
, allowing the Remix runtime to fetch pre-generated data files from the public directory, making SSG a very quick script away for folks who want it)While this might not seem like a huge win on the surface here is a summary of the benefits:
Closing that inconsistency gap also enables these future features:
Beta Was this translation helpful? Give feedback.
All reactions