Yuki Takei 6 місяців тому
батько
коміт
5de835c143
1 змінених файлів з 105 додано та 75 видалено
  1. 105 75
      apps/app/src/features/search/client/components/SearchModal.tsx

+ 105 - 75
apps/app/src/features/search/client/components/SearchModal.tsx

@@ -3,7 +3,7 @@ import Downshift, {
   type StateChangeOptions,
   type StateChangeOptions,
 } from 'downshift';
 } from 'downshift';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
-import React, { type JSX, useCallback, useEffect, useState } from 'react';
+import { type JSX, useCallback, useEffect, useMemo, useState } from 'react';
 import { Modal, ModalBody } from 'reactstrap';
 import { Modal, ModalBody } from 'reactstrap';
 
 
 import { isIncludeAiMenthion, removeAiMenthion } from '../../utils/ai';
 import { isIncludeAiMenthion, removeAiMenthion } from '../../utils/ai';
@@ -15,12 +15,11 @@ import { SearchHelp } from './SearchHelp';
 import { SearchMethodMenuItem } from './SearchMethodMenuItem';
 import { SearchMethodMenuItem } from './SearchMethodMenuItem';
 import { SearchResultMenuItem } from './SearchResultMenuItem';
 import { SearchResultMenuItem } from './SearchResultMenuItem';
 
 
-const SearchModal = (): JSX.Element => {
+const SearchModalSubstance = (): JSX.Element => {
   const [searchKeyword, setSearchKeyword] = useState('');
   const [searchKeyword, setSearchKeyword] = useState('');
   const [isMenthionedToAi, setMenthionedToAi] = useState(false);
   const [isMenthionedToAi, setMenthionedToAi] = useState(false);
 
 
   const { data: searchModalData, close: closeSearchModal } = useSearchModal();
   const { data: searchModalData, close: closeSearchModal } = useSearchModal();
-
   const router = useRouter();
   const router = useRouter();
 
 
   const changeSearchTextHandler = useCallback((searchText: string) => {
   const changeSearchTextHandler = useCallback((searchText: string) => {
@@ -42,20 +41,24 @@ const SearchModal = (): JSX.Element => {
     closeSearchModal();
     closeSearchModal();
   }, [closeSearchModal, router, searchKeyword]);
   }, [closeSearchModal, router, searchKeyword]);
 
 
-  const stateReducer = (
-    state: DownshiftState<DownshiftItem>,
-    changes: StateChangeOptions<DownshiftItem>,
-  ) => {
-    // Do not update highlightedIndex on mouse hover
-    if (changes.type === Downshift.stateChangeTypes.itemMouseEnter) {
-      return {
-        ...changes,
-        highlightedIndex: state.highlightedIndex,
-      };
-    }
-
-    return changes;
-  };
+  // Memoize stateReducer to prevent recreation on every render
+  const stateReducer = useCallback(
+    (
+      state: DownshiftState<DownshiftItem>,
+      changes: StateChangeOptions<DownshiftItem>,
+    ) => {
+      // Do not update highlightedIndex on mouse hover
+      if (changes.type === Downshift.stateChangeTypes.itemMouseEnter) {
+        return {
+          ...changes,
+          highlightedIndex: state.highlightedIndex,
+        };
+      }
+
+      return changes;
+    },
+    [],
+  );
 
 
   useEffect(() => {
   useEffect(() => {
     if (!searchModalData?.isOpened) {
     if (!searchModalData?.isOpened) {
@@ -72,71 +75,98 @@ const SearchModal = (): JSX.Element => {
     setMenthionedToAi(isIncludeAiMenthion(searchKeyword));
     setMenthionedToAi(isIncludeAiMenthion(searchKeyword));
   }, [searchKeyword]);
   }, [searchKeyword]);
 
 
-  const searchKeywordWithoutAi = removeAiMenthion(searchKeyword);
+  // Memoize AI mention removal to prevent recalculation on every render
+  const searchKeywordWithoutAi = useMemo(() =>
+    removeAiMenthion(searchKeyword),
+    [searchKeyword]
+  );
+
+  // Memoize icon selection to prevent recalculation
+  const searchIcon = useMemo(() =>
+    isMenthionedToAi ? 'psychology' : 'search',
+    [isMenthionedToAi]
+  );
+
+  // Memoize icon class to prevent string concatenation on every render
+  const iconClassName = useMemo(() =>
+    `material-symbols-outlined fs-4 me-3 ${isMenthionedToAi ? 'text-primary' : ''}`,
+    [isMenthionedToAi]
+  );
+
+  return (
+    <ModalBody className="pb-2">
+      <Downshift
+        onSelect={selectSearchMenuItemHandler}
+        stateReducer={stateReducer}
+        defaultIsOpen
+      >
+        {({
+          getRootProps,
+          getInputProps,
+          getItemProps,
+          getMenuProps,
+          highlightedIndex,
+        }) => (
+          <div {...getRootProps({}, { suppressRefError: true })}>
+            <div className="text-muted d-flex justify-content-center align-items-center p-1">
+              <span className={iconClassName}>
+                {searchIcon}
+              </span>
+              <SearchForm
+                searchKeyword={searchKeyword}
+                onChange={changeSearchTextHandler}
+                onSubmit={submitHandler}
+                getInputProps={getInputProps}
+              />
+              <button
+                type="button"
+                className="btn border-0 d-flex justify-content-center p-0"
+                onClick={closeSearchModal}
+              >
+                <span className="material-symbols-outlined fs-4 ms-3 py-0">
+                  close
+                </span>
+              </button>
+            </div>
+
+            <ul {...getMenuProps()} className="list-unstyled m-0">
+              <div className="border-top mt-2 mb-2" />
+              <SearchMethodMenuItem
+                activeIndex={highlightedIndex}
+                searchKeyword={searchKeywordWithoutAi}
+                getItemProps={getItemProps}
+              />
+              <SearchResultMenuItem
+                activeIndex={highlightedIndex}
+                searchKeyword={searchKeywordWithoutAi}
+                getItemProps={getItemProps}
+              />
+              <div className="border-top mt-2 mb-2" />
+            </ul>
+          </div>
+        )}
+      </Downshift>
+      <SearchHelp />
+    </ModalBody>
+  );
+};
+
+const SearchModal = (): JSX.Element => {
+  const { data: searchModalData, close: closeSearchModal } = useSearchModal();
+
+  // Early return for performance optimization
+  if (!searchModalData?.isOpened) {
+    return <></>;
+  }
 
 
   return (
   return (
     <Modal
     <Modal
       size="lg"
       size="lg"
-      isOpen={searchModalData?.isOpened ?? false}
+      isOpen={searchModalData.isOpened}
       toggle={closeSearchModal}
       toggle={closeSearchModal}
       data-testid="search-modal"
       data-testid="search-modal"
     >
     >
-      <ModalBody className="pb-2">
-        <Downshift
-          onSelect={selectSearchMenuItemHandler}
-          stateReducer={stateReducer}
-          defaultIsOpen
-        >
-          {({
-            getRootProps,
-            getInputProps,
-            getItemProps,
-            getMenuProps,
-            highlightedIndex,
-          }) => (
-            <div {...getRootProps({}, { suppressRefError: true })}>
-              <div className="text-muted d-flex justify-content-center align-items-center p-1">
-                <span
-                  className={`material-symbols-outlined fs-4 me-3 ${isMenthionedToAi ? 'text-primary' : ''}`}
-                >
-                  {isMenthionedToAi ? 'psychology' : 'search'}
-                </span>
-                <SearchForm
-                  searchKeyword={searchKeyword}
-                  onChange={changeSearchTextHandler}
-                  onSubmit={submitHandler}
-                  getInputProps={getInputProps}
-                />
-                <button
-                  type="button"
-                  className="btn border-0 d-flex justify-content-center p-0"
-                  onClick={closeSearchModal}
-                >
-                  <span className="material-symbols-outlined fs-4 ms-3 py-0">
-                    close
-                  </span>
-                </button>
-              </div>
-
-              <ul {...getMenuProps()} className="list-unstyled m-0">
-                <div className="border-top mt-2 mb-2" />
-                <SearchMethodMenuItem
-                  activeIndex={highlightedIndex}
-                  searchKeyword={searchKeywordWithoutAi}
-                  getItemProps={getItemProps}
-                />
-                <SearchResultMenuItem
-                  activeIndex={highlightedIndex}
-                  searchKeyword={searchKeywordWithoutAi}
-                  getItemProps={getItemProps}
-                />
-                <div className="border-top mt-2 mb-2" />
-              </ul>
-            </div>
-          )}
-        </Downshift>
-        <SearchHelp />
-      </ModalBody>
+      <SearchModalSubstance />
     </Modal>
     </Modal>
   );
   );
 };
 };