Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalidate fetched URLs #1277

Closed
Rich-Harris opened this issue Apr 29, 2021 · 10 comments · Fixed by #1303
Closed

Invalidate fetched URLs #1277

Rich-Harris opened this issue Apr 29, 2021 · 10 comments · Fixed by #1303

Comments

@Rich-Harris
Copy link
Member

Is your feature request related to a problem? Please describe.
Suppose you were building an e-commerce site. You might have a shopping cart icon in your root src/routes/$layout.svelte component. Inside src/routes/products/[category]/[id].svelte you might have an 'add to cart' button. When the user clicks the button, the cart icon should update.

One perfectly valid way to model this would be to put the user's cart on the session store, and then update the store after the POST triggered by 'add to cart' succeeds:

async function submit(e) {
  e.preventDefault(); // because we're progressively enhancing the 'add to cart' <form>

  const product = await fetch(this.action, {
    method: this.method,
    body: new FormData(this)
  });

  $session.cart.products = [...$session.cart.products, product];
}

But you might have valid reasons for not wanting to put that data in session — perhaps the cart is only visible on some parts of the app and you don't want to always pay the cost of serializing the data. Instead, you might do something like this:

<!-- src/routes/$layout.svelte -->
<script context="module">
  export async function load({ fetch }) {
    const cart = await fetch('/cart.json').then(r => r.json());
    return {
      props: { cart }
    };
  }
</script>

This creates a problem. POSTing to /cart.json will add the product(s) to the cart, but will no longer cause the cart icon to update, because the layout component has no way of knowing that the result of fetching /cart.json has changed.

Describe the solution you'd like
Since we're using the passed-in fetch, we know which URLs the layout component depends on. We could therefore expose an API like this:

+// may not be the best place for this functionality, but we can bikeshed that
+import { invalidate } from '$app/navigation';
+
async function submit(e) {
  e.preventDefault(); // because we're progressively enhancing the 'add to cart' <form>

  const product = await fetch(this.action, {
    method: this.method,
    body: new FormData(this)
  });

-  $session.cart.products = [...$session.cart.products, product];
+  invalidate('/cart.json');
}

SvelteKit could straightforwardly keep track of which load functions fetched the invalidated URL, and rerun them (assuming the components they belong to are still mounted), much as we already do for load functions that use session when that store changes value.

Describe alternatives you've considered
This is a somewhat low-tech solution. A more common approach might be to use a full-blown fat-client state management system that has a copy of the model in memory (see e.g. Firebase). One possibility would be to integrate more closely with such systems or even build one ourselves.

Personally I'm more inclined towards simple, explicit mechanisms that are easy to implement and understand; in any case this would be complementary towards the more full-blown solutions (or at the very least, not in conflict).

Other possibilities that I'm not keen on:

  • Intercepting all non-GET fetches and invalidating all load functions automatically
  • Offering a way to invalidate everything in one go, rather than invalidating specific URLs

How important is this feature to you?
It's a nice-to-have — it solves a problem that can already be solved in various ways, but arguably in a more elegant way. And it's the one feature I've seen in Remix that we don't already have a (more elegant, IMHO 💅 ) equivalent of that I'm aware of :)

(Their version re-runs all loaders indiscriminately whenever an 'action' — their word for POST/PUT/PATCH/DELETE etc requests — takes place. AFAICT it wouldn't work with external APIs, just the app's own endpoint. But it does have the nice characteristic that it runs automatically.)

@arxpoetica
Copy link
Member

arxpoetica commented Apr 30, 2021

So just to make sure I'm understanding, this is an app-level invalidation, sort of along the lines of things we said only something like Sapper/Kit could do.

The idea is that when one invalidates, it reruns every page load function (drawing on the cached response) w/ a matching path?

Hard to see any drawbacks.

@tanhauhau tanhauhau mentioned this issue Apr 30, 2021
5 tasks
@Conduitry
Copy link
Member

This sounds sensible to me.

I'm wondering now, though, what a fat-client-middle-ground thing would look like. Since props no longer need to be serializable, it feels like it might be as simple as having load wrap the props/context values it returns in writable stores, and then you can update this store from anywhere? If I'm not missing something, that sounds fairly nice to do in userland, and so I don't think we need more official support for something like that.

In cases where you don't want to bother with a fat client and where you just want to invalidate/refetch certain data, I think the proposed API seems very reasonable.

@dummdidumm
Copy link
Member

I too like this idea. The simplicity and explicitly also makes it possible to enhance this as you please. For example as Conduitry mentioned, you could then enhance your load function with stuff like store calls.

Would it be a good idea to be able to pass data along with the invalidate? Suppose you have a todo list, the load function returns a store of the todos and can control that state. Now you enter a new item, you invalidate and send along the new item. The load function could then do an optimistic update on the store it controls. Is this a good thing to be able to do or does this blow up load too much (doing too many things at once)?

@Conduitry
Copy link
Member

I think for something like that you'd just update the store yourself and then call the invalidate function, so that when load is re-run, it grabs the (hopefully same) data from the server.

If you're passing data to the invalidate function along with a URL to invalidate, it's not clear to me where that would go.

@UltraCakeBakery
Copy link

UltraCakeBakery commented Apr 30, 2021

What if you have multiple fetch calls inside one load function? Re-running the entire load function could in this case cause unnecessary strain on your API and the users bandwidth.

What if there was an alternative option to export an array of loads instead?

Rough example:

<!-- src/routes/$layout.svelte -->
<script context="module">
    export const load = [
        async ({ fetch }) => {
            const notifications = await fetch('/notifications.json').then(r => r.json());
            return {
                props: { notifications }
            }
        }, 
        async ({ fetch }) => {
            const updates = await fetch('/updates.json').then(r => r.json());
            return {
                props: { updates }
            }
        }, 
        async ({ fetch }) => {
            const cart = await fetch('/cart.json').then(r => r.json());
            return {
                props: { cart }
            }
        }, 
        async ({ fetch }) => {
            const menuItems= await fetch('/menu-items.json').then(r => r.json());
            return {
                props: { menuItems }
            }
        }
    ]
</script>

Invalidating would work the same as in the original proposal, but instead you could pass the index of the targeted load function instead:

invalidate(2); // Invalidates the `/cart.json` load

@dummdidumm
Copy link
Member

This would be another good use case for passing an explicit parameter to the load function that is defined on subsequent loads. The array feels a little boilerplate-y and maybe too inflexible for me.

Rich-Harris added a commit that referenced this issue May 2, 2021
@Rich-Harris Rich-Harris mentioned this issue May 2, 2021
5 tasks
Rich-Harris pushed a commit that referenced this issue May 4, 2021
* invalidate resource

* failing test for #1277

* fix bad this reference, strengthen test

* remove code typescript doesnt like

* rename invalidates -> dependencies

* implement batching, bypass cache

* changeset and docs

Co-authored-by: Tan Li Hau <[email protected]>
@MatthiasGrandl
Copy link

It would be nice if we could regex match resources here... It's a little annoying if you have dynamic fetches based on for example query parameters. So it would be cool if you could do something like invalidate("/test\?=.*") or simply invalidate(".*") to invalidate all resources...

My usecase is that I am planning on calling invalidate inside one component that is used by a lot of different routes that have different load functions, so making it work with invalidate currently is a little verbose.

@pzuraq
Copy link
Contributor

pzuraq commented Nov 11, 2021

I have a similar use case, we have some pretty complex fetch statements we're making base on the query params on the page, and it's a pain to have reconstruct the entire query string for each invalidate.

@n1kk
Copy link

n1kk commented Feb 4, 2022

Had the same issue, needed to invalidate some endpoints, wrote a plugin to automate it https://github.com/n1kk/vite-plugin-sveltekit-fetch-invalidate

It can automatically figure out what to invalidate if your endpoints are simple and don't have parameters in them, otherwise, you can specify a custom list.

In one of my projects it's a simple glob fetchInvalidate({ patterns: ["src/blog/**/*.mdx"] }) in other it's a bit more complicated but custom list gets the job done.

@rodryquintero
Copy link

Does invalidate also updates the URL of the current page? It appears it doesn't. I have a use case where I would like to share the URL of the page after invalidation. @Rich-Harris @Conduitry

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants