Yuki Takei 5 месяцев назад
Родитель
Сommit
59e35e1bd1

+ 22 - 17
apps/app/src/client/components/Common/ImageCropModal.tsx

@@ -76,14 +76,14 @@ const ImageCropModal: FC<Props> = (props: Props) => {
     reset();
   }, [reset]);
 
-  const onImageLoaded = (image) => {
+  // Memoize image processing functions
+  const onImageLoaded = useCallback((image) => {
     setImageRef(image);
     reset();
     return false;
-  };
-
+  }, [reset]);
 
-  const getCroppedImg = async(image: HTMLImageElement, crop: ICropOptions) => {
+  const getCroppedImg = useCallback(async(image: HTMLImageElement, crop: ICropOptions) => {
     const {
       naturalWidth: imageNaturalWidth, naturalHeight: imageNaturalHeight, width: imageWidth, height: imageHeight,
     } = image;
@@ -107,32 +107,37 @@ const ImageCropModal: FC<Props> = (props: Props) => {
       logger.error(err);
       toastError(new Error('Failed to draw image'));
     }
-  };
+  }, []);
 
   // Convert base64 Image to blob
-  const convertBase64ToBlob = async(base64Image: string) => {
+  const convertBase64ToBlob = useCallback(async(base64Image: string) => {
     const base64Response = await fetch(base64Image);
     return base64Response.blob();
-  };
+  }, []);
 
 
-  // Clear image and set isImageCrop true on modal close
-  const onModalCloseHandler = async() => {
+  // Memoize event handlers
+  const onModalCloseHandler = useCallback(async() => {
     setImageRef(null);
     onModalClose();
-  };
+  }, [onModalClose]);
 
-  // Process and save image
-  // Cropping image is optional
-  // If crop is active , the saved image is cropped image (png). Otherwise, the original image will be saved (Original size and file type)
-  const processAndSaveImage = async() => {
+  const processAndSaveImage = useCallback(async() => {
     if (imageRef && cropOptions?.width && cropOptions.height) {
       const processedImage = isCropImage ? await getCroppedImg(imageRef, cropOptions) : await convertBase64ToBlob(imageRef.src);
       // Save image to database
       onImageProcessCompleted(processedImage);
     }
     onModalCloseHandler();
-  };
+  }, [imageRef, cropOptions, isCropImage, getCroppedImg, convertBase64ToBlob, onImageProcessCompleted, onModalCloseHandler]);
+
+  const toggleCropMode = useCallback(() => setIsCropImage(!isCropImage), [isCropImage]);
+  const handleCropChange = useCallback((crop: CropOptions) => setCropOtions(crop), []);
+
+  // Early return optimization
+  if (!isShow) {
+    return <></>;
+  }
 
   return (
     <Modal isOpen={isShow} toggle={onModalCloseHandler}>
@@ -148,7 +153,7 @@ const ImageCropModal: FC<Props> = (props: Props) => {
                 src={src}
                 crop={cropOptions}
                 onImageLoaded={onImageLoaded}
-                onChange={crop => setCropOtions(crop)}
+                onChange={handleCropChange}
                 circularCrop={isCircular}
               />
             )
@@ -167,7 +172,7 @@ const ImageCropModal: FC<Props> = (props: Props) => {
                 className="form-check-input me-auto"
                 type="checkbox"
                 checked={isCropImage}
-                onChange={() => { setIsCropImage(!isCropImage) }}
+                onChange={toggleCropMode}
               />
               <label className="form-label form-check-label" htmlFor="cropImageOption">
                 { t('crop_image_modal.image_crop') }

+ 6 - 4
apps/app/src/client/components/CreateTemplateModal.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
 
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
@@ -67,9 +67,11 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
     }
   }, [createTemplate, onClose, path, t]);
 
-  const parentPath = pathUtils.addTrailingSlash(path);
+  // Memoize computed path
+  const parentPath = useMemo(() => pathUtils.addTrailingSlash(path), [path]);
 
-  const renderTemplateCard = (target: TargetType, label: LabelType) => (
+  // Memoize template card rendering function
+  const renderTemplateCard = useCallback((target: TargetType, label: LabelType) => (
     <div className="col">
       <TemplateCard
         target={target}
@@ -78,7 +80,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
         onClickHandler={() => onClickTemplateButtonHandler(label)}
       />
     </div>
-  );
+  ), [isCreating, onClickTemplateButtonHandler]);
 
   if (!isCreatable) {
     return <></>;

+ 14 - 8
apps/app/src/client/components/DescendantsPageListModal.tsx

@@ -1,6 +1,6 @@
 
 import React, {
-  useState, useMemo, useEffect, type JSX,
+  useState, useMemo, useEffect, useCallback,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
@@ -25,7 +25,7 @@ const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./De
 
 const PageTimeline = dynamic(() => import('./PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
 
-export const DescendantsPageListModal = (): JSX.Element => {
+export const DescendantsPageListModal = (): React.JSX.Element => {
   const { t } = useTranslation();
 
   const [activeTab, setActiveTab] = useState('pagelist');
@@ -74,18 +74,24 @@ export const DescendantsPageListModal = (): JSX.Element => {
     };
   }, [isSharedUser, status, t]);
 
+  // Memoize event handlers
+  const expandWindow = useCallback(() => setIsWindowExpanded(true), []);
+  const contractWindow = useCallback(() => setIsWindowExpanded(false), []);
+  const onNavSelected = useCallback((v: string) => setActiveTab(v), []);
+
   const buttons = useMemo(() => (
     <span className="me-3">
       <ExpandOrContractButton
         isWindowExpanded={isWindowExpanded}
-        expandWindow={() => setIsWindowExpanded(true)}
-        contractWindow={() => setIsWindowExpanded(false)}
+        expandWindow={expandWindow}
+        contractWindow={contractWindow}
       />
       <button type="button" className="btn btn-close ms-2" onClick={close} aria-label="Close"></button>
     </span>
-  ), [close, isWindowExpanded]);
+  ), [close, isWindowExpanded, expandWindow, contractWindow]);
 
-  if (status == null) {
+  // Early return after all hooks
+  if (status == null || !status.isOpened) {
     return <></>;
   }
 
@@ -105,7 +111,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
             activeTab={activeTab}
             navTabMapping={navTabMapping}
             breakpointToHideInactiveTabsDown="md"
-            onNavSelected={v => setActiveTab(v)}
+            onNavSelected={onNavSelected}
             hideBorderBottom
           />
         )}
@@ -115,7 +121,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
           <CustomNavDropdown
             activeTab={activeTab}
             navTabMapping={navTabMapping}
-            onNavSelected={v => setActiveTab(v)}
+            onNavSelected={onNavSelected}
           />
         )}
         <CustomTabContent

+ 15 - 6
apps/app/src/client/components/GrantedGroupsInheritanceSelectModal.tsx

@@ -1,4 +1,4 @@
-import { useState, type JSX } from 'react';
+import { useState, useCallback } from 'react';
 
 import { useTranslation } from 'react-i18next';
 import {
@@ -9,16 +9,25 @@ import {
   useGrantedGroupsInheritanceSelectModalActions, useGrantedGroupsInheritanceSelectModalStatus,
 } from '~/states/ui/modal/granted-groups-inheritance-select';
 
-const GrantedGroupsInheritanceSelectModal = (): JSX.Element => {
+const GrantedGroupsInheritanceSelectModal = (): React.JSX.Element => {
   const { t } = useTranslation();
   const { isOpened, onCreateBtnClick: _onCreateBtnClick } = useGrantedGroupsInheritanceSelectModalStatus();
   const { close: closeModal } = useGrantedGroupsInheritanceSelectModalActions();
   const [onlyInheritUserRelatedGrantedGroups, setOnlyInheritUserRelatedGrantedGroups] = useState(false);
 
-  const onCreateBtnClick = async() => {
+  // Memoize event handlers
+  const onCreateBtnClick = useCallback(async() => {
     await _onCreateBtnClick?.(onlyInheritUserRelatedGrantedGroups);
     setOnlyInheritUserRelatedGrantedGroups(false); // reset to false after create request
-  };
+  }, [_onCreateBtnClick, onlyInheritUserRelatedGrantedGroups]);
+
+  const setInheritAll = useCallback(() => setOnlyInheritUserRelatedGrantedGroups(false), []);
+  const setInheritRelatedOnly = useCallback(() => setOnlyInheritUserRelatedGrantedGroups(true), []);
+
+  // Early return optimization
+  if (!isOpened) {
+    return <></>;
+  }
 
   return (
     <Modal
@@ -37,7 +46,7 @@ const GrantedGroupsInheritanceSelectModal = (): JSX.Element => {
               className="form-check-input"
               form="formImageType"
               checked={!onlyInheritUserRelatedGrantedGroups}
-              onChange={() => { setOnlyInheritUserRelatedGrantedGroups(false) }}
+              onChange={setInheritAll}
             />
             <label className="form-check-label" htmlFor="inheritAllGroupsRadio">
               {t('modal_granted_groups_inheritance_select.inherit_all_granted_groups_from_parent')}
@@ -50,7 +59,7 @@ const GrantedGroupsInheritanceSelectModal = (): JSX.Element => {
               className="form-check-input"
               form="formImageType"
               checked={onlyInheritUserRelatedGrantedGroups}
-              onChange={() => { setOnlyInheritUserRelatedGrantedGroups(true) }}
+              onChange={setInheritRelatedOnly}
             />
             <label className="form-check-label" htmlFor="onlyInheritRelatedGroupsRadio">
               {t('modal_granted_groups_inheritance_select.only_inherit_related_groups')}

+ 10 - 12
apps/app/src/features/search/client/components/SearchPage/SearchOptionModal.tsx

@@ -1,4 +1,5 @@
 import type { FC } from 'react';
+import { useCallback } from 'react';
 import { useTranslation } from 'next-i18next';
 import { Modal, ModalBody, ModalHeader } from 'reactstrap';
 
@@ -23,23 +24,24 @@ const SearchOptionModal: FC<Props> = (props: Props) => {
     onIncludeTrashPagesSwitched,
   } = props;
 
-  const onCloseModal = () => {
+  // Memoize event handlers
+  const onCloseModal = useCallback(() => {
     if (onClose != null) {
       onClose();
     }
-  };
+  }, [onClose]);
 
-  const includeUserPagesChangeHandler = (isChecked: boolean) => {
+  const includeUserPagesChangeHandler = useCallback((isChecked: boolean) => {
     if (onIncludeUserPagesSwitched != null) {
       onIncludeUserPagesSwitched(isChecked);
     }
-  };
+  }, [onIncludeUserPagesSwitched]);
 
-  const includeTrashPagesChangeHandler = (isChecked: boolean) => {
+  const includeTrashPagesChangeHandler = useCallback((isChecked: boolean) => {
     if (onIncludeTrashPagesSwitched != null) {
       onIncludeTrashPagesSwitched(isChecked);
     }
-  };
+  }, [onIncludeTrashPagesSwitched]);
 
   return (
     <Modal size="lg" isOpen={isOpen} toggle={onCloseModal} autoFocus={false}>
@@ -53,9 +55,7 @@ const SearchOptionModal: FC<Props> = (props: Props) => {
               <input
                 className="me-2"
                 type="checkbox"
-                onChange={(e) =>
-                  includeUserPagesChangeHandler(e.target.checked)
-                }
+                onChange={useCallback((e) => includeUserPagesChangeHandler(e.target.checked), [includeUserPagesChangeHandler])}
                 checked={includeUserPages}
               />
               {t('Include Subordinated Target Page', { target: '/user' })}
@@ -66,9 +66,7 @@ const SearchOptionModal: FC<Props> = (props: Props) => {
               <input
                 className="me-2"
                 type="checkbox"
-                onChange={(e) =>
-                  includeTrashPagesChangeHandler(e.target.checked)
-                }
+                onChange={useCallback((e) => includeTrashPagesChangeHandler(e.target.checked), [includeTrashPagesChangeHandler])}
                 checked={includeTrashPages}
               />
               {t('Include Subordinated Target Page', { target: '/trash' })}