From b24bfae3e03748d78212445925384cdb3d6860f3 Mon Sep 17 00:00:00 2001 From: DenisVorop Date: Mon, 9 Oct 2023 10:37:56 +0300 Subject: [PATCH] fix: on new comment input re-renders only comment form --- src/components/GoalActivityFeed.tsx | 14 +---- src/components/GoalCommentCreateForm.tsx | 61 ++++++++++++++++++++ src/components/ProjectsPage/ProjectsPage.tsx | 7 +-- src/hooks/useGoalResource.ts | 34 +---------- src/hooks/useLSDraft.ts | 4 +- src/hooks/useLatest.ts | 11 ++++ src/hooks/useLocalStorage.ts | 13 +++-- 7 files changed, 90 insertions(+), 54 deletions(-) create mode 100644 src/components/GoalCommentCreateForm.tsx create mode 100644 src/hooks/useLatest.ts diff --git a/src/components/GoalActivityFeed.tsx b/src/components/GoalActivityFeed.tsx index fda5b6a58..c853724c8 100644 --- a/src/components/GoalActivityFeed.tsx +++ b/src/components/GoalActivityFeed.tsx @@ -18,7 +18,7 @@ import { AddCriteriaForm } from './CriteriaForm/CriteriaForm'; const ModalOnEvent = dynamic(() => import('./ModalOnEvent')); const GoalEditForm = dynamic(() => import('./GoalEditForm/GoalEditForm')); -const CommentCreateForm = dynamic(() => import('./CommentCreateForm/CommentCreateForm')); +const GoalCommentCreateForm = dynamic(() => import('./GoalCommentCreateForm')); interface GoalActivityFeedProps { goal: NonNullable; @@ -40,10 +40,7 @@ export const GoalActivityFeed = forwardRef { onGoalDelete(); onGoalDeleteConfirm?.(); @@ -148,13 +143,10 @@ export const GoalActivityFeed = forwardRef } footer={ - } renderCommentItem={(value) => ( diff --git a/src/components/GoalCommentCreateForm.tsx b/src/components/GoalCommentCreateForm.tsx new file mode 100644 index 000000000..f236283d4 --- /dev/null +++ b/src/components/GoalCommentCreateForm.tsx @@ -0,0 +1,61 @@ +import { ComponentProps, FC, useCallback } from 'react'; +import type { Comment } from '@prisma/client'; + +import { useLSDraft } from '../hooks/useLSDraft'; + +import CommentCreateForm from './CommentCreateForm/CommentCreateForm'; + +interface GoalCommentCreateFormProps { + states: ComponentProps['states']; + goalId: string; + onSubmit: (comment?: { description: string; stateId?: string }) => Promise; +} + +const GoalCommentCreateForm: FC = ({ states, goalId, onSubmit }) => { + const { + saveDraft: saveGoalCommentDraft, + resolveDraft: resolveGoalCommentDraft, + removeDraft: removeGoalCommentDraft, + } = useLSDraft('draftGoalComment', {}); + + const onGoalCommentChange = useCallback( + (comment?: { stateId?: string; description?: string }) => { + if (!comment?.description) { + removeGoalCommentDraft(goalId); + return; + } + + saveGoalCommentDraft(goalId, comment); + }, + [goalId, removeGoalCommentDraft, saveGoalCommentDraft], + ); + + const onGoalCommentCancel = useCallback(() => { + removeGoalCommentDraft(goalId); + }, [goalId, removeGoalCommentDraft]); + + const commentDraft = resolveGoalCommentDraft(goalId); + + const onGoalCommentSubmit = useCallback( + async (comment?: { description: string; stateId?: string }) => { + const data = await onSubmit(comment); + if (data) { + removeGoalCommentDraft(goalId); + } + }, + [goalId, onSubmit, removeGoalCommentDraft], + ); + + return ( + + ); +}; + +export default GoalCommentCreateForm; diff --git a/src/components/ProjectsPage/ProjectsPage.tsx b/src/components/ProjectsPage/ProjectsPage.tsx index 64253f2ec..502bb28ff 100644 --- a/src/components/ProjectsPage/ProjectsPage.tsx +++ b/src/components/ProjectsPage/ProjectsPage.tsx @@ -1,4 +1,4 @@ -import { MouseEventHandler, useCallback, useEffect, useMemo, useRef } from 'react'; +import { MouseEventHandler, useCallback, useEffect, useMemo } from 'react'; import { useRouter as useNextRouter } from 'next/router'; import dynamic from 'next/dynamic'; import NextLink from 'next/link'; @@ -41,7 +41,6 @@ export const projectsSize = 20; export const ProjectsPage = ({ user, ssrTime, params: { id }, defaultPresetFallback }: ExternalPageProps) => { const nextRouter = useNextRouter(); const [, setCurrentProjectCache] = useLocalStorage('currentProjectCache', null); - const setCurrentProjectCacheRef = useRef(setCurrentProjectCache); const { toggleFilterStar } = useFilterResource(); const utils = trpc.useContext(); @@ -137,13 +136,13 @@ export const ProjectsPage = ({ user, ssrTime, params: { id }, defaultPresetFallb useEffect(() => { if (!project) return; - setCurrentProjectCacheRef.current({ + setCurrentProjectCache({ id: project.id, title: project.title, description: project.description ?? undefined, flowId: project.flowId, }); - }, [project]); + }, [project, setCurrentProjectCache]); useWillUnmount(() => { setCurrentProjectCache(null); diff --git a/src/hooks/useGoalResource.ts b/src/hooks/useGoalResource.ts index af662d155..8995c3e30 100644 --- a/src/hooks/useGoalResource.ts +++ b/src/hooks/useGoalResource.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import { Activity, GoalAchieveCriteria } from '@prisma/client'; +import type { Activity, GoalAchieveCriteria } from '@prisma/client'; import { trpc } from '../utils/trpcClient'; import { notifyPromise } from '../utils/notifyPromise'; @@ -13,7 +13,6 @@ import { } from '../schema/criteria'; import { ModalEvent, dispatchModalEvent } from '../utils/dispatchModal'; -import { useLSDraft } from './useLSDraft'; import { useHighlightedComment } from './useHighlightedComment'; import { useReactionsResource } from './useReactionsResource'; @@ -87,11 +86,6 @@ export const useGoalResource = (fields: GoalFields, config?: InvalidateConfigura const updateGoalOwnerMutation = trpc.goal.updateOwner.useMutation(); const createGoalMutation = trpc.goal.create.useMutation(); - const { - saveDraft: saveGoalCommentDraft, - resolveDraft: resolveGoalCommentDraft, - removeDraft: removeGoalCommentDraft, - } = useLSDraft('draftGoalComment', {}); const { highlightCommentId, setHighlightCommentId } = useHighlightedComment(); const { commentReaction } = useReactionsResource(fields.reactions); @@ -178,20 +172,6 @@ export const useGoalResource = (fields: GoalFields, config?: InvalidateConfigura [removeGoalDependency, invalidate], ); - const onGoalCommentChange = useCallback( - (comment?: { stateId?: string; description?: string }) => { - if (!id) return; - - if (!comment?.description) { - removeGoalCommentDraft(id); - return; - } - - saveGoalCommentDraft(id, comment); - }, - [id, removeGoalCommentDraft, saveGoalCommentDraft], - ); - const onGoalCommentCreate = useCallback( async (comment?: { description: string; stateId?: string }, invalidateKey?: RefetchKeys | RefetchKeys[]) => { if (!comment || !id) return; @@ -204,7 +184,6 @@ export const useGoalResource = (fields: GoalFields, config?: InvalidateConfigura const [data] = await notifyPromise(promise, 'commentCreate'); if (data) { - removeGoalCommentDraft(data.id); setHighlightCommentId(data.id); invalidate(invalidateKey); @@ -212,7 +191,7 @@ export const useGoalResource = (fields: GoalFields, config?: InvalidateConfigura return data; }, - [createGoalComment, id, invalidate, removeGoalCommentDraft, setHighlightCommentId], + [createGoalComment, id, invalidate, setHighlightCommentId], ); const onGoalCommentUpdate = useCallback( @@ -234,12 +213,6 @@ export const useGoalResource = (fields: GoalFields, config?: InvalidateConfigura [invalidate, updateGoalComment], ); - const onGoalCommentCancel = useCallback(() => { - if (id) { - removeGoalCommentDraft(id); - } - }, [id, removeGoalCommentDraft]); - const onGoalCommentReactionToggle = useCallback( (id: string, invalidateKey?: RefetchKeys | RefetchKeys[]) => { return commentReaction(id, () => invalidate(invalidateKey)); @@ -433,7 +406,6 @@ export const useGoalResource = (fields: GoalFields, config?: InvalidateConfigura lastStateComment, invalidate, - resolveGoalCommentDraft, goalCreate, goalUpdate, @@ -459,10 +431,8 @@ export const useGoalResource = (fields: GoalFields, config?: InvalidateConfigura onGoalCriteriaRemove, onGoalCriteriaConvert, - onGoalCommentChange, onGoalCommentCreate, onGoalCommentUpdate, - onGoalCommentCancel, onGoalCommentReactionToggle, onGoalCommentDelete, }; diff --git a/src/hooks/useLSDraft.ts b/src/hooks/useLSDraft.ts index 1771a7f0a..ebfd46671 100644 --- a/src/hooks/useLSDraft.ts +++ b/src/hooks/useLSDraft.ts @@ -8,7 +8,7 @@ type DraftComment = DraftGoalComment[keyof DraftGoalComment]; type DraftGoalCommentKey = 'draftGoalComment'; function useLSDraft(storageKey: KDG, initialValue: Record) { - const [draft, setDraft] = useLocalStorage(storageKey, initialValue); + const [, setDraft, draftRef] = useLocalStorage(storageKey, initialValue); const saveDraft = useCallback( (id: string, draft: DraftComment) => { @@ -20,7 +20,7 @@ function useLSDraft(storageKey: KDG, initialVal [setDraft], ); - const resolveDraft = useCallback((id?: string) => (id ? draft[id] : undefined), [draft]); + const resolveDraft = useCallback((id?: string) => (id ? draftRef.current[id] : undefined), [draftRef]); const removeDraft = useCallback( (id: string) => { diff --git a/src/hooks/useLatest.ts b/src/hooks/useLatest.ts new file mode 100644 index 000000000..d79501a3f --- /dev/null +++ b/src/hooks/useLatest.ts @@ -0,0 +1,11 @@ +import { useLayoutEffect, useRef } from 'react'; + +export function useLatest(value: T) { + const ref = useRef(value); + + useLayoutEffect(() => { + ref.current = value; + }, [value]); + + return ref; +} diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index c469c6125..7b8a1b05e 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -1,7 +1,9 @@ -import { useCallback, useState } from 'react'; +import { MutableRefObject, useCallback, useState } from 'react'; import { safelyParseJson } from '../utils/safelyParseJson'; +import { useLatest } from './useLatest'; + export type LastOrCurrentProject = { id: string; title: string; flowId: string; description?: string | null } | null; export type RecentProjectsCache = Record< string, @@ -22,7 +24,7 @@ export function useLocalStorage( storageKey: T, // eslint-disable-next-line @typescript-eslint/no-explicit-any defaultValue?: any, -): [StorageKey[T], SetValue] { +): [StorageKey[T], SetValue, MutableRefObject] { const safelySetStorage = useCallback( (valueToStore: string) => { try { @@ -45,14 +47,15 @@ export function useLocalStorage( return safelyParseJson(valueToStore); }); + const storedValueRef = useLatest(storedValue); const setValue: SetValue = useCallback( (value) => { - const valueToStore = value instanceof Function ? value(storedValue) : value; + const valueToStore = value instanceof Function ? value(storedValueRef.current) : value; safelySetStorage(JSON.stringify(valueToStore)); setStoredValue(valueToStore); }, - [safelySetStorage, storedValue], + [safelySetStorage, storedValueRef], ); - return [storedValue, setValue]; + return [storedValue, setValue, storedValueRef]; }