sveltejs / sapper

The next small thing in web development, powered by Svelte
https://sapper.svelte.dev
MIT License
6.99k stars 433 forks source link

Example or solution for SSR friendly stores that can fetch and cache api resources #1627

Open claudijo opened 3 years ago

claudijo commented 3 years ago

Is your feature request related to a problem? Please describe. I am looking for a Sapper example or "native" support to render pages based on api requests in the preload function (ie. server or client side), and then cache the response on the client side. The idea is that if a fetched resource is cached, you will not need to do an additional request when navigating back to a page that is dependant on that resource.

For example, if you take the blog posts from the official sapper template installed with npx degit "sveltejs/sapper-template#rollup" my-app, each blog post will be re-fetched by the client when navigating between blog posts. A common use case, in my experience, is that you only want to fetch a resource once and then server the cached resource if available, or resort to fetching if there is a cache miss.

Describe the solution you'd like Ideally stores would be client/server agnostic. If that is not an option, an example in the docs covering this use case would be valuable.

Below I present my current solution, which I find to be quite bloated. Comments or or suggestions for better solutions would be much appreciated.

Describe alternatives you've considered

// Store with fetching and caching capabilities 
// src/stores/post.js

import { writable, get } from 'svelte/store';

export const posts = writable({});

async function fetchAndCachePost(slug) {
  // Falling back to node-fetch that is globally exposed on the server side. 
  // Note that node-fetch need the absolute url in contrast to `this.fetch` that 
  // is accessible in the `preload` function.
  const res = await fetch(`http://localhost:3000/blog/${slug}.json`);
  const post = await res.json();
  if (res.status !== 200) {
    return;
  }

  if (process.browser) {
    storePost(slug, post);
  }

  return post;
}

export function storePost(slug, post) {
  posts.update(state => {
    return {
      ...state,
      [slug]: post,
    }
  })
}

export async function getPost(slug) {
  if (process.browser) {
    const post = get(posts)[slug];
    if (post) {
      return post;
    }
  }

  return await fetchAndCachePost(slug);
}
// Modified original file to use post store from above
// src/routes/blog/[slug].svelte

<script context="module">
    import { getPost } from '../../stores/post';

    export async function preload({ params }) {
        const { slug } = params;
        const post = await getPost(slug);
        return { post, slug };
    }
</script>

<script>
    import { storePost, posts } from '../../stores/post';

    export let post;
    export let slug;

    storePost(slug, post);
</script>

<svelte:head>
    <title>{$posts[slug] && $posts[slug].title}</title>
</svelte:head>

<h1>{$posts[slug] && $posts[slug].title}</h1>

<div class="content">
    {@html $posts[slug] && $posts[slug].html}
</div>
bencates commented 3 years ago

This is already possible using the session store.

<script context="module">
  import { fetchPost } from 'some/api/layer'

  export async function preload({ params }, session) {
    if (!session.posts.hasOwnProperty(slug))
      session.posts[slug] = await fetchPost(slug);

    return { post: session.posts[slug], slug };
  }
</script>

The session will be serialized and passed to the client automatically during the SSR render process, so when the preload function re-runs client side the cache will already be warm.

I haven't tested whether mutating the session in a preload function is correctly recognized as a store mutation or not, though, so this might cause bugs if you need to reference that slice of the session store in other components.