2

When I try to subscribe to my Supabase database for realtime updates, I am able to listen to changes (Insert/Update/Delete) but my React state resets.

For example, let's say I get 5 names in the initial call and I set data to those 5 names and this displays correctly. But when I insert a new row in the database, my React Native app detects that change, and somehow my state data is again set to an empty array.

Sidenote: I've disabled RLS in the database for now. Not sure if that should have an impact though.

Any help is appreciated. Thanks!

This is the code. I've also tried to subscribe inside the async block.

const [data, setData] = useState<IProduct[]>([]);

const ReplaceObj = (newObj: IProduct) => {
    // Check existing data
    console.log("Data: ",data);
    // Remove the item from existing data
    const filteredList: IProduct[] = data.filter(
      e => e.id !== newObj.id,
    );
    // Append the new data
    setData([...filteredList, newObj]);
  }

useEffect(() => {

    (async () => {
      // Fetch initial data using Supabase queries
      const fetchInitialData = async () => {
        const {data: responseData}: any = await supabase
          .from('Products')
          .select('*');
        if (responseData) {
          const initialData: IProduct[] = responseData;
          setData(initialData);
        }
      };
      fetchInitialData();
    })();

    // Subscribe to real-time updates using Supabase subscriptions
    const Products: any = supabase
      .channel('custom-all-channel')
      .on(
        'postgres_changes',
        {event: '*', schema: 'public', table: 'Products'},
        (payload: any) => {
          console.log('PAYLOAD: \n', payload);
          if (payload?.new) {
            const newObj: IProduct = {
              affiliate_info: payload.new.affiliate_info,
              categories: payload.new.categories,
              created_at: payload.new.created_at,
              description: payload.new.description,
              homepage_url: payload.new.homepage_url,
              id: payload.new.id,
              name: payload.new.name,
            };
            ReplaceObj(newObj);
          }
        },
      )
      .subscribe();

    // Unsubscribe from real-time updates when the component unmounts
    return () => Products.unsubscribe();

  }, []);
Prateik
  • 33
  • 5

1 Answers1

2

Issue

The issue here is a stale closure over the initial data state value from the initial render cycle when the useEffect ran and instantiated the listener callback. This means any time ReplaceObj is called it will update from the closed over initial data state value.

const ReplaceObj = (newObj: IProduct) => {
  // Check existing data
  console.log("Data: ", data); // <-- closed over stale data state

  // Remove the item from existing data
  const filteredList: IProduct[] = data.filter(
    e => e.id !== newObj.id,
  );

  // Append the new data
  setData([...filteredList, newObj]);
}

Solution

Use a functional state update to correctly update from the previous state versus whatever value is closed over in callback scope.

const ReplaceObj = (newObj: IProduct) => {
  // Filter and Append the new data from existing data
  setData(data => {
    // Check existing data
    console.log("Data: ", data); // *

    return data
      .filter(e => e.id !== newObj.id)
      .concat(newObj);
  });
}

* Note: The state updater callback function should be a pure function, free from side-effect (yes, even a console log is considered a side-effect). I left it in so you can see the previous state value before update. If you want to confirm the state update then use a separate useEffect hook with a dependency on the data state to log state updates.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181