Replace `this.fetch` with `this.get`
See original GitHub issueStumbled into a bit of a rabbit hole while adding prerendering to adapter-node — because this.fetch
is expected to work exactly the same as fetch
, we have to account for the possibility that someone will this.fetch('/some-static-file.json
), which means that the prerenderer needs to know where the static files could live, which muddies the API somewhat.
It’s also just a bit of an awkward API:
- Sapper doesn’t currently handle URLs relative to the requested page (though this could easily change, and has in fact changed in the current app-utils)
- The whole
res = await this.fetch(...); data = await res.json()
dance is a bit of a pain, honestly - It’s easy to forget that you need
credentials: 'include'
if you’re usingsession
fetch
supports non-GET methods, which don’t make sense in the context ofpreload
(or load, as the case may be)
I’d like to replace it with this.get
:
<script context="module">
export async function load({ params }) {
return await this.get(`./${params.slug}.json`);
}
</script>
- Only works with server routes, not static files or external resources. Simpler
- Accepts a URL (easiest way to identify server routes, which will of course continue to work with plain
fetch
), which can be relative to the requested page - Always includes credentials
- Returns a promise for the body of the response. If the
Content-Type
isapplication/json
, isJSON.parse
d. If it’stext/*
, is returned as text. Otherwise is returned as an array buffer - Non-200 responses throw an error (augmented with
status
code), which is equivalent to callingthis.error(status, error)
, except that you can catch and handle if necessary. For common cases, this eliminates the need to add error handling around the response returned fromthis.fetch
We could also expose get
alongside the other app functions (goto
et al) for use outside preload
/load
functions. (It would probably just throw in Node.) That’s not necessary for this.get
to have value though.
Of course there are still times when it’s useful to be able to use fetch
in preload
/load
— hn.svelte.dev fetches data from https://api.hnpwa.com, for example, and it’s good to be able to do this isomorphically (rather than forcing the app to always connect to the app’s own server, like ahem some frameworks). But if it’s only to be used for external URLs, then there’s no need to use this.fetch
, we can just use regular fetch
. We can polyfill it in Node with @rollup/plugin-inject and node-fetch (which already powers this.fetch
). With this approach, many apps wouldn’t need node-fetch at all, whereas currently it has to be included because of this.fetch
.
This seems like all upside to me, except for the breakingness of the change. But if there was ever a time for breaking changes this is it. Thoughts?
Issue Analytics
- State:
- Created 3 years ago
- Comments:38 (38 by maintainers)
Top GitHub Comments
I was imagining that this would work somewhat similar to an exported site, in that the server would run the
preload
function, and track which URLs were retrieved, and note the responses somewhere. The server will have already sent the appropriate headers when it was requesting the endpoints that it writes out the responses to in the rendered HTML. The headers thing only seems to be a problem if the values of the headers are non-deterministic in some way (or vary depending onprocess.browser
) (which would be a problem anyway with Sapper’s existingpreload
handling), or ifpreload
hit the same endpoint twice with different HTTP headers (which seems really unusual).I’m not sure what the point about generated code is in relation to. I’m not seeing any generated code in Rich’s suggestion. The client-side
this.get
/this.fetch
/whatever would simply first look whether the URL it’s requesting was one of the ones that had a response baked into the HTML that was rendered for the page.This is just a fleeting thought that I’d like to write down before it evaporates. I haven’t thought it through:
Sometimes, I want to do some sort of processing of my data inside
preload
. For example, I might have this……which links objects representing states to objects representing counties. Thanks to devalue, that’s possible, because we can serialize the preloaded data even though it contains cyclical and repeated references. But devalue can’t serialize everything — it couldn’t handle
counties: counties.map(d => new County(d))
, for example.Another example, which I’m currently facing. There’s a huge difference between these:
Conceptually, the first one makes more sense: the component doesn’t care about the data in its compressed state;
preload
is the appropriate place to do that work (in the same way that the component doesn’t care about the JSON, it only cares about the deserialized result. Decompression and deserialization fall into the same category, I think). But in practical terms, the second one is much better, because otherwise we have to serialize the decompressed data for the server-rendered page, which defeats the object of compression.Those of us reading this issue understand the mechanics well enough to know to prefer the second form so that we don’t serialize and transmit the decompressed data. Many people wouldn’t.
So I wonder if we should change our approach: what if we didn’t serialize the output of
preload
, but instead serialized the inputs, and ranpreload
in the client for the initial hydration, as we do for other pages? In that case, calling something likemight cause the following to be stitched into the SSR’d page…
…which would be used to satisfy the
this.get('/some-data.json')
call in the client for the initial hydration, ensuring a) we don’t need an additional network request for that data (same as current serialization approach), and b) guaranteeing consistency between server and client renders.Additionally, using
JSON.parse
is somewhat faster than an object literal or a devalue IIFE.The ability to return functions and non-POJOs from
preload
would be an additional bonus.