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

Optimistic UI #313

Open
Jonnotie opened this issue Nov 5, 2023 · 10 comments
Open

Optimistic UI #313

Jonnotie opened this issue Nov 5, 2023 · 10 comments
Labels
enhancement New feature or request

Comments

@Jonnotie
Copy link
Sponsor

Jonnotie commented Nov 5, 2023

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 Jonnotie added the enhancement New feature or request label Nov 5, 2023
@psteinroe
Copy link
Owner

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.

@Jonnotie
Copy link
Sponsor Author

Jonnotie commented Nov 8, 2023

@psteinroe that would be worth it though :D

@Jonnotie
Copy link
Sponsor Author

Jonnotie commented Nov 9, 2023

Sponsored your work. Keep up the good work 🤘

@psteinroe
Copy link
Owner

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

@cptlchad
Copy link

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

@psteinroe
Copy link
Owner

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.

@adamstoffel
Copy link
Contributor

adamstoffel commented May 7, 2024

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.

@skamensky
Copy link

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

@psteinroe
Copy link
Owner

@skamensky it should be!

@skamensky
Copy link

@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;

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

No branches or pull requests

5 participants