Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Mutate the data without key at first #403

Closed
matigda opened this issue May 18, 2020 · 5 comments
Closed

Mutate the data without key at first #403

matigda opened this issue May 18, 2020 · 5 comments
Labels
help wanted Extra attention is needed

Comments

@matigda
Copy link

matigda commented May 18, 2020

Hey, I have hard time figuring out how to deal with usage of SWR and revalidation when key for request is not there from the beginning.

Case looks like that:

I want my guest to add product to cart. But if he doesn't have a cart then I need to create one before adding product to it.

So code looked like this at the beginning:

const {data:cart, mutate } = useSWR(getCartId(), fetchAllCartData);

const addProduct = async (product, quantity) => {
   try {
	if (!cart) {
	    await createCart(); // this method creates cart and returns cart id
	}
	await requestToAddProduct(product, quantity);
	await mutate({
		...cart,
		items: [...cart.items, product]
	});
   } catch(err) {}
}

This code works well if there is a cart id before guest tries to add product.
The issue here is that I don't have cart id when guest tries to add first product.
This code alone is not enough to revalidate, because well, there is no key to which mutate is bound to. I need to add const [isRevalidated, revalidate] = useState(false); and call it after requestToAddProduct. So even though I have most of the data required to show in my UI when requestToAddProduct ends, I can't. I need to manualy refetch whole cart.

So code now actually looks more like that:

const {data:cart, mutate } = useSWR(getCartId(), fetchAllCartData);
const [isRevalidated, revalidate] = useState(false);

const addProduct = async (product, quantity) => {
   const cartId = cart? cart.id : await createCart();
   try {
	   if (!cart) {
	     await createCart(); // this method creates cart and returns cart id
	   }
	   await requestToAddProduct(product, quantity);
           
	   if (!cart) {
	      revalidate(!isRevalidated);
	      return;
	    }
	   await mutate({
	      ...cart,
	      items: [...cart.items, product]
	   });
   } catch(err) {}
}

I tried to revalidate in createCart method but then, when I call addProduct, useSWR returns data from cache and there is also request race between request that fetches cart and the one that adds product to cart. So I often fetched a cart before product was added and then useSWR returned data from cache.
Also - the issue with this code is that I need to add isLoading variable to state. If I want to show loader when user adds product to cart I can't rely addProduct solely, because in the case I described I don't get modified cart right after requestToAddProduct and mutate. I get it after those methods AND refetch.

I think solution that solves this problem is bounding mutate not to key but to given useSWR call. Or maybe adding another param like cacheKey. But maybe I misunderstand some of the concept here.

@illuminist
Copy link

By JS closure rule, cart from useSWR cannot see the change within addProduct call so even the swr is revalidated or mutated somewhere else, the cart in addProduct will stay the same until it reachs the end of addProduct function.

You might need to access mutated cart data directly from swr cache instead.

@matigda
Copy link
Author

matigda commented May 19, 2020

By JS closure rule, cart from useSWR cannot see the change within addProduct call so even the swr is revalidated or mutated somewhere else, the cart in addProduct will stay the same until it reachs the end of addProduct function.

That's ok. Because when user adds first product, cart is empty. The issue here is that I want my cart to be updated right after I call mutate after requestToAddProduct. And cart won't be updated as I had no key to which useSWR is bound to before addToProduct was called. I mean that's how mutate is supposed to work. And it is working fine when the cart id is there.

@shuding shuding added the help wanted Extra attention is needed label Jun 10, 2020
@matigda
Copy link
Author

matigda commented Sep 19, 2020

Ok, if someone is still interested in this - here is working example with react-query. It's basically about optimistic update but without knowing all required params up front. I really don't know how to do it properly in useSWR ( you may take this code and call addProduct and see what is in cart ):

import React, {useContext} from "react";
import { useQuery, useMutation, useQueryCache } from 'react-query'
import Cookies from 'js-cookie'

const CartContext = React.createContext({});

const ReactQueryProvider = ({children} : {children: React.ReactElement}) => {

    const cache = useQueryCache();

    const getCartId = (): string | undefined => {
        return Cookies.get('cartId');
    };

    useQuery('cart', (key) => {
        // here would go request to backend to fetch cart

        // fetch(getCartId());

        return cache.getQueryData('cart');
    });

    const [createCart] = useMutation(async () => {
        // here would go request to backend to create cart

        Cookies.set('cartId', "2");
    });

    const [addProduct] = useMutation(async (product) => {

        if (!getCartId()) {
            await createCart();
        }

        // here would go request to backend for actually adding product
    }, {
        onMutate: (product: any) => {
            cache.cancelQueries('cart');

            const previousCart = cache.getQueryData('cart');

            cache.setQueryData('cart', (oldCart: any[] | undefined = []) : any[] => [...oldCart, product])

            return () => cache.setQueryData('cart', previousCart)
        },

        onError: (err, newTodo, rollback: () => void) => rollback(),

        onSuccess: () => {
            cache.invalidateQueries('cart')
        },
    });



  return <CartContext.Provider
      value={{
          cart: cache.getQueryData('cart'),
          addProduct,
      }}
    >
      {children}
  </CartContext.Provider>
};

export const useCart = () => {
  return useContext(CartContext);
};

export default ReactQueryProvider;

it is working due to the fact that key in react-query is just cache key. You can pass there whatever you wishes and you can still use functions inside the fetcher from the outside - like getCartId in this example. In useSWR I have to pass cartId as a param to the fetcher and due to the fact that I may not have it when there is new guest on my page - optimistic UI update after adding very first product to your guest cart ( which have to be created at the moment you are trying to add your product to cart ) required workarounds. I may be wrong, but then please, enlighten me :)

@sergiodxa
Copy link
Contributor

You can totally do the same with SWR, set your key as cart and then in your fetcher (which could be unique per key) you can read your cart id from the cookie.

Then, in your mutate calls you can call it with cart as key and you can pass a function as second argument, this function will receive the current cached data for the key cart, whatever you return will be the new value, and you can even use an async function if you wish and the resolved value from the promise will be the new cached value.

@TommySorensen
Copy link

@sergiodxa Do you have a example of the code you are suggesting?

@huozhi huozhi closed this as completed Jun 17, 2021
@vercel vercel locked and limited conversation to collaborators Jun 17, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

6 participants