veza/apps/web/src/docs/OPTIMISTIC_UPDATES.md

5.1 KiB

Optimistic Updates Guide

FE-API-018: Optimistic UI Updates for Better UX

This guide explains how to implement optimistic updates in Veza frontend components to provide instant feedback to users.

What are Optimistic Updates?

Optimistic updates allow the UI to update immediately before the server responds, making the app feel faster and more responsive. If the request fails, the UI is rolled back to the previous state.

Utilities Available

1. createOptimisticUpdate

For general optimistic updates:

import { useQueryClient } from '@tanstack/react-query';
import { createOptimisticUpdate } from '@/utils/optimisticUpdates';

const queryClient = useQueryClient();
const mutation = useMutation({
  mutationFn: updatePlaylist,
  ...createOptimisticUpdate({
    queryClient,
    queryKeys: [['playlist', playlistId], ['playlists']],
    optimisticData: (variables) => ({
      ...currentPlaylist,
      ...variables,
    }),
  }),
});

2. createArrayOptimisticUpdate

For array operations (add, remove, update):

import { useQueryClient } from '@tanstack/react-query';
import { createArrayOptimisticUpdate } from '@/utils/optimisticUpdates';

const queryClient = useQueryClient();

// Add item
const addMutation = useMutation({
  mutationFn: addTrackToPlaylist,
  ...createArrayOptimisticUpdate({
    queryClient,
    queryKeys: [['playlist', playlistId]],
    arrayQueryKey: ['playlist', playlistId, 'tracks'],
    operation: 'add',
    optimisticItem: (variables) => ({
      id: `temp-${Date.now()}`,
      track_id: variables.trackId,
      ...variables,
    }),
  }),
});

// Remove item
const removeMutation = useMutation({
  mutationFn: removeTrackFromPlaylist,
  ...createArrayOptimisticUpdate({
    queryKeys: [['playlist', playlistId]],
    arrayQueryKey: ['playlist', playlistId, 'tracks'],
    operation: 'remove',
    matchItem: (item, variables) => item.id === variables.trackId,
  }),
});

3. createToggleOptimisticUpdate

For toggle operations (like/unlike, follow/unfollow):

import { useQueryClient } from '@tanstack/react-query';
import { createToggleOptimisticUpdate } from '@/utils/optimisticUpdates';

const queryClient = useQueryClient();
const likeMutation = useMutation({
  mutationFn: () => likeTrack(trackId),
  ...createToggleOptimisticUpdate({
    queryClient,
    queryKeys: [['track', trackId], ['tracks']],
    currentValue: isLiked,
    updateCount: true,
    countDelta: 1,
  }),
  onSuccess: () => {
    showSuccess('Ajouté aux favoris');
  },
  onError: (error) => {
    showError(error.message || "Erreur lors de l'ajout aux favoris");
  },
});

Best Practices

  1. Always provide rollback: The utilities automatically rollback on error, but you can provide custom rollback logic if needed.

  2. Invalidate queries on success: The utilities automatically invalidate queries on settle, but you can also invalidate on success for immediate refresh.

  3. Show user feedback: Always show success/error messages to inform users of the result.

  4. Handle edge cases: Consider what happens if the optimistic data structure doesn't match the actual response.

  5. Use for fast operations: Optimistic updates work best for operations that typically complete quickly (< 500ms).

Examples

Example 1: Updating a Playlist

const queryClient = useQueryClient();
const updateMutation = useMutation({
  mutationFn: (data: UpdatePlaylistRequest) => updatePlaylist(playlistId, data),
  ...createOptimisticUpdate({
    queryClient,
    queryKeys: [['playlist', playlistId]],
    optimisticData: (variables) => ({
      ...playlist,
      ...variables,
    }),
  }),
  onSuccess: () => {
    showSuccess('Playlist mise à jour');
  },
});

Example 2: Adding a Comment

const queryClient = useQueryClient();
const addCommentMutation = useMutation({
  mutationFn: (content: string) => createComment(trackId, content),
  ...createArrayOptimisticUpdate({
    queryClient,
    queryKeys: [['trackComments', trackId]],
    arrayQueryKey: ['trackComments', trackId],
    operation: 'add',
    optimisticItem: (variables) => ({
      id: `temp-${Date.now()}`,
      content: variables,
      user: currentUser,
      created_at: new Date().toISOString(),
    }),
  }),
  onSuccess: () => {
    showSuccess('Commentaire ajouté');
  },
});

Example 3: Following a User

const queryClient = useQueryClient();
const followMutation = useMutation({
  mutationFn: () => followUser(userId),
  ...createToggleOptimisticUpdate({
    queryClient,
    queryKeys: [['user', userId], ['users']],
    currentValue: isFollowing,
    updateCount: true,
    countDelta: 1,
  }),
  onSuccess: () => {
    showSuccess('Vous suivez maintenant cet utilisateur');
  },
});

When NOT to Use Optimistic Updates

  • Critical operations: Don't use for operations that must be confirmed (e.g., payments, deletions)
  • Slow operations: Don't use for operations that take > 2 seconds
  • Complex state: Don't use if the optimistic update is too complex or error-prone
  • Server-side validation: Don't use if the server response might differ significantly from the optimistic data