T
TanStack•10mo ago
harsh-harlequin

useEffect not triggered when data change

I have this :
const { data: playlistItems } = usePlaylistItems(playlist?.id);
const { data: isAllowedToEdit } = usePlaylistIsAllowedToEdit(playlist?.id);
console.log('in table playlist items', playlistItems);
React.useEffect(() => {
console.log('new playlist items', playlistItems);
}, [playlistItems]);
const { data: playlistItems } = usePlaylistItems(playlist?.id);
const { data: isAllowedToEdit } = usePlaylistIsAllowedToEdit(playlist?.id);
console.log('in table playlist items', playlistItems);
React.useEffect(() => {
console.log('new playlist items', playlistItems);
}, [playlistItems]);
When my playlistItems change I can see the first console.log but the one in useEffect isnt triggered.. why ? I check that because I have a react table showing playlistItems but the table isnt updated when fresh data arrived... Thx for your help !
28 Replies
correct-apricot
correct-apricot•10mo ago
It's because of the way useEffect works. It doesn't work if you pass an array as the dependency
foreign-sapphire
foreign-sapphire•10mo ago
Hi, can you show us usePlaylistItems please?
harsh-harlequin
harsh-harlequinOP•10mo ago
ohh yes its working with playlistItems.length as depency TO prevet making multiple requests I do this for getting the playlist :
export const usePlaylistFull = (playlistId: number) => {
const queryClient = useQueryClient();
const supabase = useSupabaseClient();
return useQuery({
queryKey: playlistKeys.detail(playlistId),
queryFn: async () => {
if (!playlistId) throw Error('Missing playlist id');
const { data, error } = await supabase
.from('playlist')
.select(`
*,
user(*),
items:playlist_item(
*,
movie(*)
),
guests:playlist_guest(
*,
user:user(*)
)
`)
.eq('id', playlistId)
.order('rank', { ascending: true, referencedTable: 'playlist_item' })
.returns<Playlist[]>()
.single();
if (error || !data) throw error;

// Set the playlist items and guests in the queryClient
queryClient.setQueryData(playlistKeys.items(playlistId), data.items);
queryClient.setQueryData(playlistKeys.guests(playlistId), data.guests);
const { items, guests, ...playlistData } = data;
return playlistData;
// return data;
},
enabled: !!playlistId,
});
}
export const usePlaylistFull = (playlistId: number) => {
const queryClient = useQueryClient();
const supabase = useSupabaseClient();
return useQuery({
queryKey: playlistKeys.detail(playlistId),
queryFn: async () => {
if (!playlistId) throw Error('Missing playlist id');
const { data, error } = await supabase
.from('playlist')
.select(`
*,
user(*),
items:playlist_item(
*,
movie(*)
),
guests:playlist_guest(
*,
user:user(*)
)
`)
.eq('id', playlistId)
.order('rank', { ascending: true, referencedTable: 'playlist_item' })
.returns<Playlist[]>()
.single();
if (error || !data) throw error;

// Set the playlist items and guests in the queryClient
queryClient.setQueryData(playlistKeys.items(playlistId), data.items);
queryClient.setQueryData(playlistKeys.guests(playlistId), data.guests);
const { items, guests, ...playlistData } = data;
return playlistData;
// return data;
},
enabled: !!playlistId,
});
}
SO here I manually set Query data for playlistItems for making only one requst. But the usePlayItems do this for being invalidate (only items) :
export const usePlaylistItems = (playlistId?: number) => {
const supabase = useSupabaseClient();
return useQuery({
queryKey: playlistKeys.items(playlistId as number),
queryFn: async () => {
if (!playlistId) throw Error('Missing playlist id');
const { data, error } = await supabase
.from('playlist_item')
.select(`
*,
movie(*)
`)
.eq('playlist_id', playlistId)
.order('rank', { ascending: true })
if (error) throw error;
return data;
},
enabled: !!playlistId,
});
}
export const usePlaylistItems = (playlistId?: number) => {
const supabase = useSupabaseClient();
return useQuery({
queryKey: playlistKeys.items(playlistId as number),
queryFn: async () => {
if (!playlistId) throw Error('Missing playlist id');
const { data, error } = await supabase
.from('playlist_item')
.select(`
*,
movie(*)
`)
.eq('playlist_id', playlistId)
.order('rank', { ascending: true })
if (error) throw error;
return data;
},
enabled: !!playlistId,
});
}
So my problem seems to be with React Table Because with this :
interface DataTableProps {
playlist: Playlist;
playlistItems: PlaylistItem[];
}

export default function PlaylistTable({
playlist,
playlistItems,
}: DataTableProps) {

const table = useReactTable({
data: playlistItems,
columns,
initialState: {
pagination: {
pageSize: 1001,
},
},
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
globalFilter,
},
interface DataTableProps {
playlist: Playlist;
playlistItems: PlaylistItem[];
}

export default function PlaylistTable({
playlist,
playlistItems,
}: DataTableProps) {

const table = useReactTable({
data: playlistItems,
columns,
initialState: {
pagination: {
pageSize: 1001,
},
},
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
globalFilter,
},
PLaylistItems is updated in realtime, but it seems when playlistItems change, the table dont change. I dont really know what to do for updating the table
foreign-sapphire
foreign-sapphire•10mo ago
It's not a problem with array as a dependency. It's a reference problem. One thing that can cause this issue is this one
foreign-sapphire
foreign-sapphire•10mo ago
React Query Render Optimizations
An advanced guide to minimize component re-renderings when using React Query
harsh-harlequin
harsh-harlequinOP•10mo ago
I dont really understand because my key dont change for items. I probably miss understand the post but my key is : ['playlist', id, 'items']. WHen I receive realtime change i do this :
onSuccess: (newPlaylistItems) => {
queryClient.setQueryData(playlistKeys.items(playlistId), [...newPlaylistItems]);
},
onSuccess: (newPlaylistItems) => {
queryClient.setQueryData(playlistKeys.items(playlistId), [...newPlaylistItems]);
},
Items are associated to a playlist id
foreign-sapphire
foreign-sapphire•10mo ago
The key is ok. Try to set structuralSharing: false on usePlaylistItems and see if it changes anything.
harsh-harlequin
harsh-harlequinOP•10mo ago
Ohh its working now But do using structuralSharing: false can cause issue ?
foreign-sapphire
foreign-sapphire•10mo ago
It's an optimization mechanism that combines oldData and newData and only changes items that are different. If you have a deeply nested object or array, this may sometimes lead to unpredicted behavior. Frankly speaking, it's not a necessity, it's only an optimization technique, so it shouldn't break anything if you turn it off.
harsh-harlequin
harsh-harlequinOP•10mo ago
I dont really understand this structual sharing here, because I update the entire items with for one specific playlist id. I probably miss something, but in the example here there is multiple id of todo
harsh-harlequin
harsh-harlequinOP•10mo ago
There is a way to optmise speed when updating react query data ? Because doing this :
try {
queryClient.setQueryData(playlistKeys.items(playlist?.id as number), (data: PlaylistItem[]) => {
if (!data) return null;
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(data, oldIndex, newIndex);
});

await updatePlaylistItem({
id: active.id,
rank: over?.data.current?.sortable.index + 1
})
} catch (error) {
queryClient.setQueryData(playlistKeys.items(playlist?.id as number), (data: PlaylistItem[]) => {
if (!data) return null;
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(data, newIndex, oldIndex);
});
toast.error("Une erreur s\'est produite");
}
try {
queryClient.setQueryData(playlistKeys.items(playlist?.id as number), (data: PlaylistItem[]) => {
if (!data) return null;
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(data, oldIndex, newIndex);
});

await updatePlaylistItem({
id: active.id,
rank: over?.data.current?.sortable.index + 1
})
} catch (error) {
queryClient.setQueryData(playlistKeys.items(playlist?.id as number), (data: PlaylistItem[]) => {
if (!data) return null;
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(data, newIndex, oldIndex);
});
toast.error("Une erreur s\'est produite");
}
Is not smouth :
harsh-harlequin
harsh-harlequinOP•10mo ago
Doing this instead with local state :
try {
setPlaylistItems((data) => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(data, oldIndex, newIndex);
});
await updatePlaylistItem({
id: active.id,
rank: over?.data.current?.sortable.index + 1
})
} catch (error) {
setPlaylistItems((data) => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(data, newIndex, oldIndex);
});
toast.error("Une erreur s\'est produite");
}
try {
setPlaylistItems((data) => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(data, oldIndex, newIndex);
});
await updatePlaylistItem({
id: active.id,
rank: over?.data.current?.sortable.index + 1
})
} catch (error) {
setPlaylistItems((data) => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(data, newIndex, oldIndex);
});
toast.error("Une erreur s\'est produite");
}
foreign-sapphire
foreign-sapphire•10mo ago
It's hard to say. Optimization techniques will differ depending on your code structure. There's no one solution to this, every case is different and requires a closer look. You need to profile your app and see bottlenecks, then address them one by one. Maybe too many components are dependent on PlaylistItems and they all rerender at the same time, maybe you need to move use playlist items closer to where you need them, to not cause rerenders higher up in the component tree, like you did with a local state.
harsh-harlequin
harsh-harlequinOP•10mo ago
That weird ebcause my usePlaylistItems is only use one time 😅
foreign-sapphire
foreign-sapphire•10mo ago
No description
foreign-sapphire
foreign-sapphire•10mo ago
No description
harsh-harlequin
harsh-harlequinOP•10mo ago
I only use this for being able to invalidate items when I add one from another page 😅
foreign-sapphire
foreign-sapphire•10mo ago
Why don't you invalidate query by a key, to save 1 request? Premature optimization is the root of all evil 😅
harsh-harlequin
harsh-harlequinOP•10mo ago
What do u means query by query ? When I add movie to a playlist, I invalidate the ['playlist', id, 'items'] key Or u means invalidate directly ['playlist', id]
foreign-sapphire
foreign-sapphire•10mo ago
Instead of doing queryClient.setQueryData you may do queryClient.invalidateQueries({ queryKey: ['playlist', id, 'items'] })
foreign-sapphire
foreign-sapphire•10mo ago
Mastering Mutations in React Query
Learn all about the concept of performing side effects on the server with React Query.
foreign-sapphire
foreign-sapphire•10mo ago
This article explains the difference between the two and when you should use one over the other.
harsh-harlequin
harsh-harlequinOP•10mo ago
Well when I dont need to query the database, like change the rank of an item, I use setQueryData, when I add a movie to the playlist from outside of the palylist page I use invalidateQueries and when I receive realtime event change, I only query the new items and add it to the list with setQueryData. I dont know if its a good way to do it but its for optimise my backend request
foreign-sapphire
foreign-sapphire•10mo ago
That's what I'm talking about. You are doing it manually with a setQueryData to save a few requests. Most of the time you want to use invalidateQueries with a key. It doesn't make an immediate request if no component is watching this query. It will fire a new request when components that use this query mount on the page. setQueryData sets data directly to the cache, but it doesn't prevent the request from firing if you don't configure staleTime. So you don't save any requests in this case. Of course, I don't see the full picture, maybe you set staleTime, and requests actually don't fire after setQueryData, but in general, this method should be used in very specific cases. Not only it take more code to write setQueryData than invalidateQuery, but it also does not provide any benefits if done wrong.
harsh-harlequin
harsh-harlequinOP•10mo ago
Tell me if Im wrong but in my case, when I use setQueryData that means the element is already show in page. For example in playlist page where items are rendered. I have a button for example to delete an item. This button gonna make a query to my db to delete this item. And for not refetch the entire playlist, I use setQueryData to delete manually in my list of items the concerned one. If I use invalidateQueries its gonna refetch the entire playlist right ?
foreign-sapphire
foreign-sapphire•10mo ago
Yes. What you describe is a good case for optimistic update. But don't worry to much about your requests or querying the DB. DB is millions times faster then BE, and BE is generally faster than FE. So in this case you should worry about your UI working smoothly, not the BE optimization
harsh-harlequin
harsh-harlequinOP•10mo ago
Yeah ahah but my db is sometime kinda slow (Im self hosting Supabase) so I wanna optimise some query For the changing rank, I find something, using queryCache for items but also local state for the rendering:
useEffect(() => {
if (playlistItems) {
setPlaylistItemsRender(playlistItems);
}
}, [playlistItems]);

if (!playlist) return null;

return (
<>
<PlaylistHeader playlist={playlist} totalRuntime={playlistItems?.reduce((total: number, item: PlaylistItem) => total + (item?.movie?.runtime ?? 0), 0)} />
<div className="p-4">
{playlistItemsRender ? <PlaylistTable playlist={playlist} playlistItems={playlistItemsRender} setPlaylistItems={setPlaylistItemsRender} /> : null}
</div>
</>
);
useEffect(() => {
if (playlistItems) {
setPlaylistItemsRender(playlistItems);
}
}, [playlistItems]);

if (!playlist) return null;

return (
<>
<PlaylistHeader playlist={playlist} totalRuntime={playlistItems?.reduce((total: number, item: PlaylistItem) => total + (item?.movie?.runtime ?? 0), 0)} />
<div className="p-4">
{playlistItemsRender ? <PlaylistTable playlist={playlist} playlistItems={playlistItemsRender} setPlaylistItems={setPlaylistItemsRender} /> : null}
</div>
</>
);
Its working ahah
foreign-sapphire
foreign-sapphire•10mo ago
Don't get me wrong, there's no right or wrong here. I'm just giving you another perspective on a matter. It's all trade-offs. If you see that in your case using setQueryData is beneficial then go for it. I'm just saying that maybe you should try to consider a different approach. Maybe adding some pagination to playlistItem to get data in smaller portions to not overload your DB, or optimizing the data structure will help you get the best of both worlds 🙂 As I've told you, the specific approach to optimization will depend on a number of factors.

Did you find this page helpful?