Optimistic UI updates with React Native and React Query - 31/12/2025

About optmistic updates using a React Native app as example

Optimistic UI updates are a technique where changes are reflected in the user interface (UI) before being confirmed by the backend. This approach enhances the user experience by providing instant feedback, making the app feel more responsive and fluid.

This article demonstrates how to implement optimistic updates in a React Native application using react-query, SQLite, and a local database for managing likes on posts.

Why Use Optimistic Updates?

Implementation Walkthrough

We’ll explore optimistic updates through a simple example: liking or unliking posts in a React Native app. The core idea is to update the UI as soon as the user interacts, then reconcile with the database asynchronously.

Github repository:

1. Setup

The application uses the following technologies:

2. Code Breakdown

Fetching Posts with react-query

The app fetches posts from the SQLite database and determines if the user has liked each post:

const { data: posts, isLoading, refetch } = useQuery({
  queryKey: ['posts'],
  queryFn: async () => {
    const result = await db.getAllAsync(`
      SELECT 
        posts.*, 
        CASE 
          WHEN EXISTS (
            SELECT 1 FROM posts_likes WHERE post_id = posts.id AND user_id = ?
          ) THEN 1 
          ELSE 0 
        END AS is_liked
      FROM posts
    `, [userId]);
    return result;
  },
});

This query retrieves posts with additional metadata (is_liked) indicating the user’s like status.

Mutation for Optimistic Updates

The mutation toggles the like status of a post and performs optimistic UI updates:

const togglePostMutation = useMutation({
  mutationFn: async (post) => {
    const likeExists = await db.getFirstAsync(
      'SELECT COUNT(*) as count FROM posts_likes WHERE post_id = ? AND user_id = ?',
      [post.id, userId]
    );

    const newLikedStatus = likeExists?.count === 0;
    const likeCountChange = newLikedStatus ? 1 : -1;
    const updatedLikesCount = post.likes_count + likeCountChange;

    if (newLikedStatus) {
      await db.runAsync('INSERT INTO posts_likes (post_id, user_id) VALUES (?, ?)', [post.id, userId]);
    } else {
      await db.runAsync('DELETE FROM posts_likes WHERE post_id = ? AND user_id = ?', [post.id, userId]);
    }

    await db.runAsync('UPDATE posts SET likes_count = ? WHERE id = ?', [updatedLikesCount, post.id]);

    return {
      ...post,
      is_liked: newLikedStatus,
      likes_count: updatedLikesCount,
    };
  },
  onMutate: async (post) => {
    await queryClient.cancelQueries(['posts']);
    const previousPosts = queryClient.getQueryData(['posts']);

    queryClient.setQueryData(['posts'], (oldPosts) =>
      oldPosts?.map((p) =>
        p.id === post.id
          ? { ...p, is_liked: !post.is_liked, likes_count: post.likes_count + (post.is_liked ? -1 : 1) }
          : p
      )
    );

    return { previousPosts };
  },
  onError: (error, _, context) => {
    if (context?.previousPosts) {
      queryClient.setQueryData(['posts'], context.previousPosts);
    }
  },
  onSuccess: (updatedPost) => {
    queryClient.setQueryData(['posts'], (oldPosts) =>
      oldPosts?.map((post) => (post.id === updatedPost.id ? updatedPost : post))
    );
  },
  onSettled: () => {
    queryClient.invalidateQueries(['posts']);
  },
});

Key aspects:

User Interaction

The user can like or unlike a post by tapping a button:

const togglePost = (post) => {
  togglePostMutation.mutate(post);
};

The UI reflects the change immediately:

<TouchableOpacity onPress={() => togglePost(item)}>
  <Text style={styles.postAction}>
    {item.is_liked ? 'Unlike' : 'Like'}
  </Text>
</TouchableOpacity>

3. UI Feedback

The app’s FlatList dynamically renders posts with updated like counts:

<FlatList
  data={posts}
  keyExtractor={(item) => item.id.toString()}
  renderItem={({ item }) => (
    <View style={styles.post}>
      <Text>{item.title}</Text>
      <Text>❤️ {item.likes_count} | 💬 {item.comments_count}</Text>
      <TouchableOpacity onPress={() => togglePost(item)}>
        <Text>{item.is_liked ? 'Unlike' : 'Like'}</Text>
      </TouchableOpacity>
    </View>
  )}
/>

Benefits of This Implementation

Conclusion

Optimistic UI updates provide a smoother and more responsive experience for users by reducing perceived latency. With libraries like react-query, managing optimistic updates becomes efficient and straightforward. By combining this with SQLite for local data handling, developers can create powerful and engaging mobile applications.