mirror of https://github.com/usememos/memos.git
perf(react-query): fix context re-renders and improve type safety
Optimizes React Query migration with performance and consistency improvements: Performance: - Memoize AuthContext and InstanceContext provider values to prevent unnecessary re-renders - Convert InstanceContext getter functions to useMemo hooks - Fix refetchSettings to avoid state dependency that caused frequent recreations Type Safety: - Replace 'any' types in useAttachmentQueries with proper protobuf types - Add Attachment and ListAttachmentsRequest type imports Query Key Consistency: - Replace hardcoded ["users", "stats"] with userKeys.stats() factory function - Ensures consistent cache key management across mutations Developer Experience: - Rename unused useCurrentUser to useCurrentUserQuery to avoid confusion - Add documentation explaining AuthContext-based vs React Query current user hooks - Update internal references in useNotifications and useTagCounts All changes verified with TypeScript compilation and build tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f87f728b0f
commit
b2e2b6426c
|
|
@ -3,6 +3,7 @@ import { useMemo, useRef } from "react";
|
|||
import { toast } from "react-hot-toast";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { memoKeys } from "@/hooks/useMemoQueries";
|
||||
import { userKeys } from "@/hooks/useUserQueries";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { EditorContent, EditorMetadata, EditorToolbar, FocusModeExitButton, FocusModeOverlay } from "./components";
|
||||
|
|
@ -119,7 +120,7 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
|
|||
// Invalidate React Query cache to refresh memo lists across the app
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: memoKeys.lists() }),
|
||||
queryClient.invalidateQueries({ queryKey: ["users", "stats"] }),
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.stats() }),
|
||||
]);
|
||||
|
||||
// Reset editor state to initial values
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { createContext, type ReactNode, useCallback, useContext, useState } from "react";
|
||||
import { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from "react";
|
||||
import { clearAccessToken } from "@/auth-state";
|
||||
import { authServiceClient, shortcutServiceClient, userServiceClient } from "@/connect";
|
||||
import { userKeys } from "@/hooks/useUserQueries";
|
||||
|
|
@ -114,23 +114,31 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
}, [queryClient]);
|
||||
|
||||
const refetchSettings = useCallback(async () => {
|
||||
if (!state.currentUser) return;
|
||||
const settings = await fetchUserSettings(state.currentUser.name);
|
||||
setState((prev) => ({ ...prev, ...settings }));
|
||||
}, [state.currentUser, fetchUserSettings]);
|
||||
// Use functional setState to get current user without including state in dependencies
|
||||
setState((prev) => {
|
||||
if (!prev.currentUser) return prev;
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
...state,
|
||||
initialize,
|
||||
logout,
|
||||
refetchSettings,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
// Fetch settings asynchronously
|
||||
fetchUserSettings(prev.currentUser.name).then((settings) => {
|
||||
setState((current) => ({ ...current, ...settings }));
|
||||
});
|
||||
|
||||
return prev;
|
||||
});
|
||||
}, [fetchUserSettings]);
|
||||
|
||||
// Memoize context value to prevent unnecessary re-renders of consumers
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
...state,
|
||||
initialize,
|
||||
logout,
|
||||
refetchSettings,
|
||||
}),
|
||||
[state, initialize, logout, refetchSettings],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { create } from "@bufbuild/protobuf";
|
||||
import { createContext, type ReactNode, useCallback, useContext, useState } from "react";
|
||||
import { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from "react";
|
||||
import { instanceServiceClient } from "@/connect";
|
||||
import { updateInstanceConfig } from "@/instance-config";
|
||||
import {
|
||||
|
|
@ -48,29 +48,30 @@ export function InstanceProvider({ children }: { children: ReactNode }) {
|
|||
isLoading: true,
|
||||
});
|
||||
|
||||
const getGeneralSetting = (): InstanceSetting_GeneralSetting => {
|
||||
// Memoize derived settings to prevent unnecessary recalculations
|
||||
const generalSetting = useMemo((): InstanceSetting_GeneralSetting => {
|
||||
const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}GENERAL`);
|
||||
if (setting?.value.case === "generalSetting") {
|
||||
return setting.value.value;
|
||||
}
|
||||
return create(InstanceSetting_GeneralSettingSchema, {});
|
||||
};
|
||||
}, [state.settings]);
|
||||
|
||||
const getMemoRelatedSetting = (): InstanceSetting_MemoRelatedSetting => {
|
||||
const memoRelatedSetting = useMemo((): InstanceSetting_MemoRelatedSetting => {
|
||||
const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}MEMO_RELATED`);
|
||||
if (setting?.value.case === "memoRelatedSetting") {
|
||||
return setting.value.value;
|
||||
}
|
||||
return create(InstanceSetting_MemoRelatedSettingSchema, {});
|
||||
};
|
||||
}, [state.settings]);
|
||||
|
||||
const getStorageSetting = (): InstanceSetting_StorageSetting => {
|
||||
const storageSetting = useMemo((): InstanceSetting_StorageSetting => {
|
||||
const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}STORAGE`);
|
||||
if (setting?.value.case === "storageSetting") {
|
||||
return setting.value.value;
|
||||
}
|
||||
return create(InstanceSetting_StorageSettingSchema, {});
|
||||
};
|
||||
}, [state.settings]);
|
||||
|
||||
const initialize = useCallback(async () => {
|
||||
setState((prev) => ({ ...prev, isLoading: true }));
|
||||
|
|
@ -125,21 +126,21 @@ export function InstanceProvider({ children }: { children: ReactNode }) {
|
|||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InstanceContext.Provider
|
||||
value={{
|
||||
...state,
|
||||
generalSetting: getGeneralSetting(),
|
||||
memoRelatedSetting: getMemoRelatedSetting(),
|
||||
storageSetting: getStorageSetting(),
|
||||
initialize,
|
||||
fetchSetting,
|
||||
updateSetting,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</InstanceContext.Provider>
|
||||
// Memoize context value to prevent unnecessary re-renders of consumers
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
...state,
|
||||
generalSetting,
|
||||
memoRelatedSetting,
|
||||
storageSetting,
|
||||
initialize,
|
||||
fetchSetting,
|
||||
updateSetting,
|
||||
}),
|
||||
[state, generalSetting, memoRelatedSetting, storageSetting, initialize, fetchSetting, updateSetting],
|
||||
);
|
||||
|
||||
return <InstanceContext.Provider value={value}>{children}</InstanceContext.Provider>;
|
||||
}
|
||||
|
||||
export function useInstance() {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { attachmentServiceClient } from "@/connect";
|
||||
import type { Attachment, ListAttachmentsRequest } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
|
||||
// Query keys factory
|
||||
export const attachmentKeys = {
|
||||
all: ["attachments"] as const,
|
||||
lists: () => [...attachmentKeys.all, "list"] as const,
|
||||
list: (filters?: any) => [...attachmentKeys.lists(), filters] as const,
|
||||
list: (filters?: Partial<ListAttachmentsRequest>) => [...attachmentKeys.lists(), filters] as const,
|
||||
details: () => [...attachmentKeys.all, "detail"] as const,
|
||||
detail: (name: string) => [...attachmentKeys.details(), name] as const,
|
||||
};
|
||||
|
|
@ -26,7 +27,7 @@ export function useCreateAttachment() {
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (attachment: any) => {
|
||||
mutationFn: async (attachment: Attachment) => {
|
||||
const result = await attachmentServiceClient.createAttachment({ attachment });
|
||||
return result;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { create } from "@bufbuild/protobuf";
|
|||
import { FieldMaskSchema } from "@bufbuild/protobuf/wkt";
|
||||
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { memoServiceClient } from "@/connect";
|
||||
import { userKeys } from "@/hooks/useUserQueries";
|
||||
import type { ListMemosRequest, Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { ListMemosRequestSchema, MemoSchema } from "@/types/proto/api/v1/memo_service_pb";
|
||||
|
||||
|
|
@ -89,7 +90,7 @@ export function useCreateMemo() {
|
|||
// Add new memo to cache
|
||||
queryClient.setQueryData(memoKeys.detail(newMemo.name), newMemo);
|
||||
// Invalidate user stats
|
||||
queryClient.invalidateQueries({ queryKey: ["users", "stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.stats() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -139,7 +140,7 @@ export function useUpdateMemo() {
|
|||
// Invalidate lists to refresh
|
||||
queryClient.invalidateQueries({ queryKey: memoKeys.lists() });
|
||||
// Invalidate user stats
|
||||
queryClient.invalidateQueries({ queryKey: ["users", "stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.stats() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -162,7 +163,7 @@ export function useDeleteMemo() {
|
|||
// Invalidate lists
|
||||
queryClient.invalidateQueries({ queryKey: memoKeys.lists() });
|
||||
// Invalidate user stats
|
||||
queryClient.invalidateQueries({ queryKey: ["users", "stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.stats() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,19 @@ export const userKeys = {
|
|||
};
|
||||
|
||||
/**
|
||||
* Hook to get the current authenticated user.
|
||||
* Hook to get the current authenticated user via React Query.
|
||||
*
|
||||
* NOTE: This hook is currently UNUSED in favor of the AuthContext-based
|
||||
* useCurrentUser hook (src/hooks/useCurrentUser.ts) which provides the same
|
||||
* data from AuthContext. The AuthContext fetches user data on app initialization
|
||||
* and stores it in React state, avoiding unnecessary re-fetches.
|
||||
*
|
||||
* This hook is kept for potential future use if we decide to fully migrate
|
||||
* auth state to React Query, but currently all components use the AuthContext version.
|
||||
*
|
||||
* Data is cached for 5 minutes as auth state changes infrequently.
|
||||
*/
|
||||
export function useCurrentUser() {
|
||||
export function useCurrentUserQuery() {
|
||||
return useQuery({
|
||||
queryKey: userKeys.currentUser(),
|
||||
queryFn: async () => {
|
||||
|
|
@ -85,7 +94,7 @@ export function useShortcuts() {
|
|||
* Only fetches when a user is authenticated.
|
||||
*/
|
||||
export function useNotifications() {
|
||||
const { data: currentUser } = useCurrentUser();
|
||||
const { data: currentUser } = useCurrentUserQuery();
|
||||
|
||||
return useQuery({
|
||||
queryKey: userKeys.notifications(),
|
||||
|
|
@ -106,7 +115,7 @@ export function useNotifications() {
|
|||
* @param forCurrentUser - If true, fetches only current user's tags; if false, fetches all public tags
|
||||
*/
|
||||
export function useTagCounts(forCurrentUser = false) {
|
||||
const { data: currentUser } = useCurrentUser();
|
||||
const { data: currentUser } = useCurrentUserQuery();
|
||||
|
||||
return useQuery({
|
||||
queryKey: forCurrentUser ? [...userKeys.stats(), "tagCounts", "current"] : [...userKeys.stats(), "tagCounts", "all"],
|
||||
|
|
|
|||
Loading…
Reference in New Issue