supabase-cache-helpers icon indicating copy to clipboard operation
supabase-cache-helpers copied to clipboard

Optimistic UI

Open Jonnotie opened this issue 1 year ago • 10 comments

Is your feature request related to a problem? Please describe. When I'm waiting for my database to process a request, I'd like to see the UI already change.

Describe the solution you'd like When inserting a new row with an undefined ID, I need to wait for the server to then update my local state. It would be cool to have this feel more instant.

Describe alternatives you've considered I'm currently winging it with Jotai, but that doesn't work when inserting.

Additional context https://swr.vercel.app/blog/swr-v2.en-US#mutation-and-optimistic-ui

Jonnotie avatar Nov 05 '23 19:11 Jonnotie

thanks for opening the issue! I was thinking about this already. Will have to check how to integrate it properly though, since the integration of the "smart" cache updates must be largely refactored.

psteinroe avatar Nov 06 '23 12:11 psteinroe

@psteinroe that would be worth it though :D

Jonnotie avatar Nov 08 '23 21:11 Jonnotie

Sponsored your work. Keep up the good work 🤘

Jonnotie avatar Nov 09 '23 23:11 Jonnotie

Sponsored your work. Keep up the good work 🤘

thank you! ❤️ let me know if you have further feedback, or know anyone interested in building out adapters for svelte & co

psteinroe avatar Nov 10 '23 08:11 psteinroe

What is the status here? Optimistic UI already possible with react-query?

cptlchad avatar Mar 12 '24 19:03 cptlchad

no, and I am not working on this right now. I am not sure if this is even a good idea at this point, because we won't have a ways to revert an optimistic update if the mutation fails on the backend.

psteinroe avatar Mar 13 '24 09:03 psteinroe

FWIW, I was able to use the "Via the UI" optimistic pattern from the react query docs by doing something like this:

  const { data } = useQuery(
    supabase.from('my_table').select('id,other_columns'),
  )

  const {
    mutateAsync,
    isPending,
    variables,
  } = useUpsertMutation(supabase.from('my_table'), ['id'])

  const uiData= useMemo(
    () => isPending ? merge([], data, variables) : data,
    [data, isPending, variables],
  )

You might need more complex merge logic for other scenarios, but this works for me with lodash merge() because I'm always upserting the entire data set.

adamstoffel avatar May 07 '24 19:05 adamstoffel

Is the onMutate parameter exposed somehow?

If so, we can handle our own optimistic UI logic .

See onMutate here

https://tanstack.com/query/v5/docs/framework/react/reference/useMutation

skamensky avatar Jul 04 '24 17:07 skamensky

@skamensky it should be!

psteinroe avatar Jul 04 '24 17:07 psteinroe

@psteinroe , Ah, I didn't understand that the third argument is passed to react-query as is. This library is super underated!

@Jonnotie, below is an example of how to do optimistic updates. I agree with @psteinroe that it might not be a good idea to bake this into the library, because handling failure use cases is probably going to be case by case basis. For example, do you revert the data to the non error state? Or do you keep it in the error state and let the user fix it, e.g. if it's form data.

Additionally, the library would need to double its memory usage to keep track of both the 'ui' state and the true query state.

In any case, here is a simple way of implementing optimistic updates:

import React, { useEffect, useState } from 'react';
import { QueryClient, QueryClientProvider} from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useQuery,useSubscription,useUpdateMutation} from "@supabase-cache-helpers/postgrest-react-query";

import { supabase } from './supabaseClient';

const queryClient = new QueryClient();

const useUsers = () => {
  return useQuery(
    supabase
      .from('users')
      .select('id,name')
      .order('id', { ascending: false })
      .limit(50),
    {
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
    }
  );
};
 
const UserList = () => {
  const { data, error, isLoading } = useUsers();
  // this is the key to the idea. We maintain to copies of our data. This one is to present to the user.
  const [uiData,setUiData] = useState(data);

  useEffect(()=>{
    setUiData(data)
  },[data])


  useSubscription(
    supabase,
    'user-insert-channel',
    {
      event: '*',
      table: 'users',
      schema: 'public',
    },
    ['id'],
    {
      callback: (payload) => {
        console.log('Change received!', payload);
      },
    }
  );

  const { mutateAsync } = useUpdateMutation(
    supabase
    .from('users'),
    ['id'],
    "id,name",
    {
      onSuccess: () => console.log('Success!'),
      onMutate:(updateData)=>{
        console.log(uiData);
        const newUiData = uiData.map(currentData=>{
          if(currentData.id==updateData.id){
            return updateData;
          }
          return currentData;
        })
        // before the update is even attempted, update the UI to show the new projected state of the data
        setUiData(newUiData);
      },
      onError:(err,oldData)=>{
        console.error("Error during update. Error:",err,"Data we tried updating:",oldData);
        // in case of error, rollback to the "known good state"
        setUiData(data);
      }
    }
  );

  const doUpdate= async (user)=>{
    const userClone = Object.assign({},user)
    // random dumb update
    userClone.name=userClone.name+1;
    await mutateAsync(userClone)
  }
  
  if (!uiData) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return (
    <ul>
      {uiData.map((user) => (
        <><li key={user.id}>{user.id} -{user.name}    <button onClick={()=>{doUpdate(user)}}>Update</button></li></>
      ))}
    </ul>
  );
};


function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <h1>User List</h1>
        <UserList />
      </div>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

export default App;

skamensky avatar Jul 04 '24 22:07 skamensky