Skip to content

Commit

Permalink
Merge pull request #109 from bambu-group-03/feature/performance-supreme
Browse files Browse the repository at this point in the history
Performance supreme 🏎️⚡
  • Loading branch information
LuisParedes1 committed Dec 6, 2023
2 parents fcfc56d + afa11ba commit 28a94f7
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 57 deletions.
98 changes: 87 additions & 11 deletions src/api/snaps/use-add-snap.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import 'react-native-get-random-values';

import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { AxiosError } from 'axios';
import { showMessage } from 'react-native-flash-message';
import { createMutation } from 'react-query-kit';
import { v4 as uuidv4 } from 'uuid';

import type { UserType } from '@/core/auth/utils';

import { client } from '../common';
import type { Snap } from './types';
Expand All @@ -14,19 +18,91 @@ type Variables = {
};
type Response = Snap;

export const useAddSnap = createMutation<Response, Variables, AxiosError>({
mutationFn: async (variables) =>
client
.content({
url: '/api/feed/post',
method: 'POST',
data: variables,
})
.then((response) => {
type SnapsFeedResponse = {
pages: Array<{
snaps: Snap[];
}>;
};

type SnapQueryFeedKey = ['snaps', string | undefined];

/**
* Custom hook to add a new snap.
* It uses optimistic UI updates to add the new snap to the UI before the server confirms.
*
* @param userId - The ID of the user adding the snap.
* @returns The mutation object with methods to trigger the addition.
*/
export const useAddSnap = (user: UserType | undefined) => {
const queryClient = useQueryClient();

return useMutation(
(variables: Variables) =>
// API call to post the new snap
client.content.post('/api/feed/post', variables).then((response) => {
console.log('response.data by useAddSnap', response.data);
return response.data;
}),
});
{
// Optimistically update the UI before the server responds
onMutate: async (newSnapData: Variables) => {
// Define the query key based on the user ID
const queryKey: SnapQueryFeedKey = ['snaps', user?.id];
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(queryKey);

// Snapshot the previous value
const previousSnaps =
queryClient.getQueryData<SnapsFeedResponse>(queryKey);

// Optimistically update to the new value
queryClient.setQueryData<SnapsFeedResponse>(queryKey, (old) => {
const tempId = uuidv4();
const newSnap: Snap = {
id: tempId,
...newSnapData,
author: user!.id,
fullname: `${user!.first_name || ''} ${user!.last_name || ''}`,
profile_photo_url: user!.profile_photo_id || '',
username: user!.username || '',
created_at: new Date().toISOString(),
// Set default values for other properties
favs: 0,
has_faved: false,
has_liked: false,
has_shared: false,
is_shared_by: [],
likes: 0,
num_replies: 0,
parent_id: '',
shares: 0,
visibility: newSnapData.privacy,
};
console.log('newSnap by useAddSnap', newSnap);
return {
...old,
pages: [
{ snaps: [newSnap, ...(old?.pages[0].snaps || [])] },
...(old?.pages.slice(1) || []),
],
};
});

return { previousSnaps };
},
onSuccess: () => {
const queryKey: SnapQueryFeedKey = ['snaps', user!.id];
queryClient.invalidateQueries(queryKey);
},
onError: (error: AxiosError, newSnapData: Variables, context: any) => {
if (context?.previousSnaps) {
const queryKey: SnapQueryFeedKey = ['snaps', user!.id];
queryClient.setQueryData(queryKey, context.previousSnaps);
}
},
}
);
};

export const useAddReplyMutation = () => {
const queryClient = useQueryClient();
Expand Down
126 changes: 101 additions & 25 deletions src/api/snaps/use-snaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,52 +105,128 @@ export const getSnap = createQuery<Snap, SnapVariables, AxiosError>({
},
});

type SnapQueryKey = [string, { snap_id?: string; user_id?: string }];

// Update Snap Mutation
type UpdateSnapArgs = {
user_id: string;
snap_id: string;
content: string;
};
type SnapsFeedResponse = {
pages: Array<{
snaps: Snap[];
}>;
};
type SnapQueryFeedKey = ['snaps', string | undefined];
/**
* Custom hook to update a snap.
* This hook uses optimistic UI update to immediately reflect changes in the UI
* and then syncs with the server.
*
* @returns The mutation object with methods to trigger the update.
*/
export const useUpdateSnapMutation = () => {
const queryClient = useQueryClient();

return useMutation(
({
user_id,
snap_id,
content,
}: {
user_id: string;
snap_id: string;
content: string;
}) =>
({ user_id, snap_id, content }: UpdateSnapArgs) =>
// API call to update the snap
client.content.put(`/api/feed/update_snap`, {
user_id,
snap_id,
content,
}),
{
// Optimistically update the UI before the server responds
onMutate: async (updatedSnap: UpdateSnapArgs) => {
await queryClient.cancelQueries(['snaps', updatedSnap.user_id]);

const previousSnaps = queryClient.getQueryData<SnapsFeedResponse>([
'snaps',
updatedSnap.user_id,
]);

queryClient.setQueryData<SnapsFeedResponse>(
['snaps', updatedSnap.user_id],
(old) => {
if (!old) return;

const newPages = old.pages.map((page) => ({
...page,
snaps: page.snaps.map((snap) =>
snap.id === updatedSnap.snap_id
? { ...snap, content: updatedSnap.content }
: snap
),
}));

return { ...old, pages: newPages };
}
);

return { previousSnaps };
},
onSuccess: () => {
// Invalidate and refetch snaps to reflect the update
queryClient.invalidateQueries<SnapQueryKey>(['/api/feed/snap']);
// Invalidate and refetch the snaps query to ensure data consistency
queryClient.invalidateQueries(['snaps']);
},
onError: (error: AxiosError) => {
// Handle error
console.error('Error updating snap:', error);
onError: (error: AxiosError, variables: UpdateSnapArgs, context: any) => {
// Rollback the optimistic update in case of error
if (context?.previousSnaps) {
queryClient.setQueryData(
['snaps', variables.user_id],
context.previousSnaps
);
}
},
}
);
};

// Delete Snap Mutation
export const useDeleteSnapMutation = () => {
/**
* Custom hook to delete a snap.
* It uses optimistic UI updates to remove the snap from the UI immediately
* before confirming the deletion from the server.
*
* @param userId The ID of the user performing the deletion.
* @returns The mutation object with methods to trigger the deletion.
*/
export const useDeleteSnapMutation = (userId: string) => {
const queryClient = useQueryClient();

return useMutation(
(snap_id: string) => client.content.delete(`/api/feed/snap/${snap_id}`),
(snap_id: string) =>
// API call to delete the snap
client.content.delete(`/api/feed/snap/${snap_id}`),
{
// Optimistically update the UI before the server responds
onMutate: async (snapId: string) => {
const queryKey: SnapQueryFeedKey = ['snaps', userId];
await queryClient.cancelQueries(queryKey);

const previousSnaps =
queryClient.getQueryData<SnapsFeedResponse>(queryKey);

queryClient.setQueryData<SnapsFeedResponse>(queryKey, (old) => ({
...old,
pages:
old?.pages.map((page) => ({
...page,
snaps: page.snaps.filter((snap) => snap.id !== snapId),
})) ?? [],
}));

return { previousSnaps };
},
onSuccess: () => {
// Invalidate and refetch snaps to reflect the deletion
queryClient.invalidateQueries<SnapQueryKey>(['/api/feed/snap']);
// Invalidate and refetch the snaps query to ensure data consistency
const queryKey: SnapQueryFeedKey = ['snaps', userId];
queryClient.invalidateQueries(queryKey);
},
onError: (error: AxiosError) => {
// Handle error
console.error('Error deleting snap:', error);
onError: (error: AxiosError, snapId: string, context: any) => {
// Rollback the optimistic update in case of error
if (context?.previousSnaps) {
const queryKey: SnapQueryFeedKey = ['snaps', userId];
queryClient.setQueryData(queryKey, context.previousSnaps);
}
},
}
);
Expand Down
7 changes: 6 additions & 1 deletion src/navigation/feed-navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
import * as React from 'react';

import type { Snap as SnapType } from '@/api';
import { AddSnap, Feed, Snap } from '@/screens';
import type { UserType } from '@/core/auth/utils';
import { AddSnap, Snap } from '@/screens';
import Feed from '@/screens/feed/list';
import ProfileScreen from '@/screens/profile/profile-screen';
import { Pressable, View } from '@/ui';

import { GoToLogout } from './auth-navigator';
Expand All @@ -15,6 +18,7 @@ export type FeedStackParamList = {
Snap: { snap: SnapType };
AddSnap: undefined;
Auth: undefined;
UserProfile: { user: UserType };
};

const Stack = createNativeStackNavigator<FeedStackParamList>();
Expand Down Expand Up @@ -48,6 +52,7 @@ export const FeedNavigator = () => {
</Stack.Group>

<Stack.Screen name="AddSnap" component={AddSnap} />
<Stack.Screen name="UserProfile" component={ProfileScreen} />
</Stack.Navigator>
);
};
22 changes: 15 additions & 7 deletions src/screens/feed/compose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import {
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
import { zodResolver } from '@hookform/resolvers/zod';
import { Picker } from '@react-native-picker/picker';
import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { showMessage } from 'react-native-flash-message';
import { z } from 'zod';

import type { Snap } from '@/api';
import { useAddSnap } from '@/api';
import { getUserState } from '@/core';
import type { UserType } from '@/core/auth/utils';
import { Image, TouchableOpacity, View } from '@/ui';
import { Button, ControlledInput, showErrorMessage } from '@/ui';
import { Button, ControlledInput } from '@/ui';

const SNAP_VISIBLE = 1;
const SNAP_HIDDEN = 2;
Expand All @@ -33,15 +35,16 @@ export const Compose = ({ user }: { user: UserType | undefined }) => {
resolver: zodResolver(schema),
});

const { mutate: addSnap, isLoading } = useAddSnap();
const currentUser = getUserState();

const privacyOptions = useMemo(
() => ['Everyone can see', 'Only Followers'],
[]
);
const [privacy, setPrivacyOption] = useState<string>(privacyOptions[0]);
const addSnapMutation = useAddSnap(currentUser);
const { isLoading } = addSnapMutation;

const { navigate } = useNavigation();
useEffect(() => {
setValue(
'privacy',
Expand All @@ -50,23 +53,28 @@ export const Compose = ({ user }: { user: UserType | undefined }) => {
}, [privacy, setValue, privacyOptions]);

const onSubmit = (data: FormType) => {
console.log(data);
addSnap(
addSnapMutation.mutate(
{
...data,
user_id: currentUser?.id,
parent_id: '',
privacy: privacy === privacyOptions[0] ? SNAP_VISIBLE : SNAP_HIDDEN,
},
{
onSuccess: () => {
onSuccess: (createdSnap: Snap) => {
console.log('onSuccess', createdSnap);
// responseData should be the snap data
navigate('Snap', { snap: createdSnap });
showMessage({
message: 'Snap added successfully',
type: 'success',
});
},
onError: () => {
showErrorMessage('Error adding post');
showMessage({
message: 'Error adding snap',
type: 'danger',
});
},
}
);
Expand Down
Loading

0 comments on commit 28a94f7

Please sign in to comment.