Skip to content

Commit

Permalink
refactor(features/article): add mutation keys (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmgasques authored Aug 10, 2023
1 parent 185e026 commit d9552fa
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 84 deletions.
8 changes: 8 additions & 0 deletions src/entities/article/api/articleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ export const articleKeys = {
root: ['article'],
slug: (slug: string) => [...articleKeys.article.root, slug],
},

mutation: {
create: () => [...articleKeys.article.root, 'create'],
delete: () => [...articleKeys.article.root, 'delete'],
update: () => [...articleKeys.article.root, 'update'],
favorite: () => [...articleKeys.article.root, 'favorite'],
unfavorite: () => [...articleKeys.article.root, 'unfavorite'],
},
};

type UseInfinityArticlesProps = {
Expand Down
7 changes: 4 additions & 3 deletions src/features/article/create/api/createArticle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import {
} from '~shared/api/realworld';

export const useCreateArticle = () =>
useMutation<articleApi.Article, GenericErrorModel, NewArticleDto, unknown>(
async (article: NewArticleDto) => {
useMutation<articleApi.Article, GenericErrorModel, NewArticleDto, unknown>({
mutationKey: articleApi.articleKeys.mutation.create(),
mutationFn: async (article: NewArticleDto) => {
const response = await realworldApi.articles.createArticle({
article,
});

return articleApi.mapArticle(response.data.article);
},
);
});
10 changes: 7 additions & 3 deletions src/features/article/delete/api/deleteArticle.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { useMutation } from '@tanstack/react-query';
import { articleApi } from '~entities/article';
import { GenericErrorModel, realworldApi } from '~shared/api/realworld';

export const useDeleteArticle = () =>
useMutation<any, GenericErrorModel, string, unknown>(async (slug: string) => {
const response = await realworldApi.articles.deleteArticle(slug);
return response.data;
useMutation<any, GenericErrorModel, string, unknown>({
mutationKey: articleApi.articleKeys.mutation.delete(),
mutationFn: async (slug: string) => {
const response = await realworldApi.articles.deleteArticle(slug);
return response.data;
},
});
142 changes: 69 additions & 73 deletions src/features/article/favorite/base/api/baseModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type ArticlesInfinityData = InfiniteData<articleApi.Article[]>;
type MutateFnType = typeof realworldApi.articles.createArticleFavorite;

export const useMutateFavoriteArticle = (
mutationKey: string[],
mutateFn: MutateFnType,
queryClient: QueryClient,
) =>
Expand All @@ -20,84 +21,79 @@ export const useMutateFavoriteArticle = (
articleQueryKey: string[];
prevArticle: articleApi.Article;
}
>(
async (article: articleApi.Article) => {
>({
mutationKey,
mutationFn: async (article: articleApi.Article) => {
const response = await mutateFn(article.slug);
return articleApi.mapArticle(response.data.article);
},
// We have to optimistic update article as part of articles list and single article to avoid desynchronize when user favorite article then instant switch beetwen single page / articles list etc... and have old state before our query refetched.
onMutate: async (newArticle) => {
const articlesQueryKey = articleApi.articleKeys.articles.root;
const articleQueryKey = articleApi.articleKeys.article.slug(
newArticle.slug,
);

// Cancel any articles and article with slug refetches
await queryClient.cancelQueries({ queryKey: articlesQueryKey });
await queryClient.cancelQueries({ queryKey: articleQueryKey });

// Snapshot the previous article.
const prevArticle: articleApi.Article = {
...newArticle,
favorited: !newArticle.favorited,
favoritesCount: newArticle.favorited
? newArticle.favoritesCount - 1
: newArticle.favoritesCount + 1,
};

// Optimistically update to the new value
queryClient.setQueriesData<ArticlesInfinityData>(
articlesQueryKey,
/* c8 ignore start */
(prevInfinityData) => {
if (!prevInfinityData) return undefined;
return updateInfinityData(prevInfinityData, newArticle);
},
/* c8 ignore end */
);

queryClient.setQueryData<articleApi.Article>(articleQueryKey, newArticle);

// Return a context object with the snapshotted value and query keys
return { articlesQueryKey, articleQueryKey, prevArticle };
},

{
// We have to optimistic update article as part of articles list and single article to avoid desynchronize when user favorite article then instant switch beetwen single page / articles list etc... and have old state before our query refetched.
onMutate: async (newArticle) => {
const articlesQueryKey = articleApi.articleKeys.articles.root;
const articleQueryKey = articleApi.articleKeys.article.slug(
newArticle.slug,
);

// Cancel any articles and article with slug refetches
await queryClient.cancelQueries({ queryKey: articlesQueryKey });
await queryClient.cancelQueries({ queryKey: articleQueryKey });

// Snapshot the previous article.
const prevArticle: articleApi.Article = {
...newArticle,
favorited: !newArticle.favorited,
favoritesCount: newArticle.favorited
? newArticle.favoritesCount - 1
: newArticle.favoritesCount + 1,
};

// Optimistically update to the new value
queryClient.setQueriesData<ArticlesInfinityData>(
articlesQueryKey,
/* c8 ignore start */
(prevInfinityData) => {
if (!prevInfinityData) return undefined;
return updateInfinityData(prevInfinityData, newArticle);
},
/* c8 ignore end */
);

queryClient.setQueryData<articleApi.Article>(
articleQueryKey,
newArticle,
);

// Return a context object with the snapshotted value and query keys
return { articlesQueryKey, articleQueryKey, prevArticle };
},

// If the mutation fails,
// use the context returned from onMutate to roll back
onError: (_error, _variables, context) => {
if (!context) return;

const { articlesQueryKey, articleQueryKey, prevArticle } = context;

queryClient.setQueriesData<ArticlesInfinityData>(
articlesQueryKey,
/* c8 ignore start */
(newInfinityData) => {
if (!newInfinityData) return undefined;
return updateInfinityData(newInfinityData, prevArticle);
},
/* c8 ignore end */
);

queryClient.setQueryData<articleApi.Article>(
articleQueryKey,
prevArticle,
);
},
// If the mutation fails,
// use the context returned from onMutate to roll back
onError: (_error, _variables, context) => {
if (!context) return;

const { articlesQueryKey, articleQueryKey, prevArticle } = context;

queryClient.setQueriesData<ArticlesInfinityData>(
articlesQueryKey,
/* c8 ignore start */
(newInfinityData) => {
if (!newInfinityData) return undefined;
return updateInfinityData(newInfinityData, prevArticle);
},
/* c8 ignore end */
);

queryClient.setQueryData<articleApi.Article>(
articleQueryKey,
prevArticle,
);
},

// Always refetch after error or success:
onSettled: (_data, _error, _variables, context) => {
if (!context) return;
// Always refetch after error or success:
onSettled: (_data, _error, _variables, context) => {
if (!context) return;

const { articlesQueryKey, articleQueryKey } = context;
const { articlesQueryKey, articleQueryKey } = context;

queryClient.invalidateQueries({ queryKey: articlesQueryKey });
queryClient.invalidateQueries({ queryKey: articleQueryKey });
},
queryClient.invalidateQueries({ queryKey: articlesQueryKey });
queryClient.invalidateQueries({ queryKey: articleQueryKey });
},
);
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { QueryClient } from '@tanstack/react-query';
import { articleApi } from '~entities/article';
import { realworldApi } from '~shared/api/realworld';
import { useMutateFavoriteArticle } from '../../base';

export const useMutationFavoriteArticle = (queryClient: QueryClient) =>
useMutateFavoriteArticle(
articleApi.articleKeys.mutation.favorite(),
realworldApi.articles.createArticleFavorite,
queryClient,
);
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { QueryClient } from '@tanstack/react-query';
import { articleApi } from '~entities/article';
import { realworldApi } from '~shared/api/realworld';
import { useMutateFavoriteArticle } from '../../base';

export const useMutationUnfavoriteArticle = (queryClient: QueryClient) =>
useMutateFavoriteArticle(
articleApi.articleKeys.mutation.unfavorite(),
realworldApi.articles.deleteArticleFavorite,
queryClient,
);
13 changes: 8 additions & 5 deletions src/features/article/update/api/updateArticle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ export const useUpdateArticle = () =>
GenericErrorModel,
UpdateArticleProps,
unknown
>(async ({ slug, article }: UpdateArticleProps) => {
const response = await realworldApi.articles.updateArticle(slug, {
article,
});
>({
mutationKey: articleApi.articleKeys.mutation.update(),
mutationFn: async ({ slug, article }: UpdateArticleProps) => {
const response = await realworldApi.articles.updateArticle(slug, {
article,
});

return articleApi.mapArticle(response.data.article);
return articleApi.mapArticle(response.data.article);
},
});

0 comments on commit d9552fa

Please sign in to comment.