From 69b21364752a8938ca26f5b4dba4b31c5719507b Mon Sep 17 00:00:00 2001 From: Sanwal Sulehri Date: Tue, 18 Feb 2025 20:41:26 +0500 Subject: [PATCH] Can we use React useMemo to improve performance? #591 solved (#613) --- components/Layout.tsx | 32 ++++++++++--------- components/Picker/LanguagePicker.tsx | 39 ++++++++++++------------ components/Picker/TagPicker.tsx | 27 +++++++++------- components/Repository/RepositoryList.tsx | 21 ++++++++----- components/Repository/SearchBar.tsx | 15 ++++++--- components/Sidebar.tsx | 16 +++++----- 6 files changed, 84 insertions(+), 66 deletions(-) diff --git a/components/Layout.tsx b/components/Layout.tsx index 6afa5beb..cd41908f 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { useMemo } from "react"; import { AppDataProvider } from "../context/AppDataContext"; import { Header } from "./Header"; import { Sidebar } from "./Sidebar"; @@ -9,16 +9,20 @@ type LayoutProps = { children: React.ReactNode; }; -export const Layout = ({ children }: LayoutProps) => ( -
- -
-
-
- - {children} -
-
- -
-); +export const Layout = ({ children }: LayoutProps) => { + const memoizedChildren = useMemo(() => children, [children]); + + return ( +
+ +
+
+
+ + {memoizedChildren} +
+
+ +
+ ); +}; diff --git a/components/Picker/LanguagePicker.tsx b/components/Picker/LanguagePicker.tsx index f99bdda9..dc5babed 100644 --- a/components/Picker/LanguagePicker.tsx +++ b/components/Picker/LanguagePicker.tsx @@ -1,6 +1,6 @@ import { faChevronDown } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { CountableLanguage } from "../../types"; import { SectionTitle } from "../SectionTitle"; import ActiveTagButton from "./ActiveTagButton"; @@ -19,10 +19,13 @@ export const LanguagePicker = ({ activeTagId, languages, onLanguagePage }: Langu setIsCollapsed(true); }, [onLanguagePage, activeTagId]); + // Memoize the list of languages to prevent unnecessary re-rendering + const memoizedLanguages = useMemo(() => languages, [languages]); + // Toggle the collapsible sidebar - const toggleCollapsible = () => { - setIsCollapsed(!isCollapsed); - }; + const toggleCollapsible = useCallback(() => { + setIsCollapsed((prev) => !prev); + }, []); return (
@@ -47,21 +50,19 @@ export const LanguagePicker = ({ activeTagId, languages, onLanguagePage }: Langu isCollapsed ? "max-h-0" : "max-h-96" } ${isCollapsed ? "sm:max-h-full" : ""}`} > - {languages.map((language) => { - return ( - - ); - })} + {memoizedLanguages.map((language) => ( + + ))}
); diff --git a/components/Picker/TagPicker.tsx b/components/Picker/TagPicker.tsx index 61f3d5c8..4d7899f0 100644 --- a/components/Picker/TagPicker.tsx +++ b/components/Picker/TagPicker.tsx @@ -1,6 +1,6 @@ import { faChevronDown } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { CountableTag } from "../../types"; import { ShowMoreButton } from "../Button/ShowMoreButton"; import { SectionTitle } from "../SectionTitle"; @@ -28,16 +28,21 @@ export const TagPicker = ({ tags, activeTagId, onTagPage }: TagPickerProps) => { setIsCollapsed(true); }, [activeTagId]); - const toggleCollapsible = () => { - setIsCollapsed(!isCollapsed); - }; + // Memoize the tags array to avoid unnecessary recalculations on each render + const memoizedTags = useMemo(() => tags, [tags]); - const handleShowMore = () => { - setLimit((limit) => limit + limitStep); - }; - const handleShowLess = () => { + const toggleCollapsible = useCallback(() => { + setIsCollapsed((prev) => !prev); + }, []); + + // Memoize the showMore and showLess handlers + const handleShowMore = useCallback(() => { + setLimit((prev) => prev + limitStep); + }, [limitStep]); + + const handleShowLess = useCallback(() => { setLimit(limitStep); - }; + }, [limitStep]); return (
@@ -65,13 +70,13 @@ export const TagPicker = ({ tags, activeTagId, onTagPage }: TagPickerProps) => { {activeTagId && isCollapsed && }
- {tags.slice(0, limit).map((tag) => { + {memoizedTags.slice(0, limit).map((tag) => { return ( { filterRepositoriesByTag, filterRepositoriesByLanguage } = useAppData(); - let repos: Repository[] = repositories; + // Memoizing the filtered repositories based on languageId and tagId + const repos: Repository[] = useMemo(() => { + let filteredRepos = repositories; - if (languageId) { - repos = filterRepositoriesByLanguage(languageId); - } + if (languageId) { + filteredRepos = filterRepositoriesByLanguage(languageId); + } - if (tagId) { - repos = filterRepositoriesByTag(tagId); - } + if (tagId) { + filteredRepos = filterRepositoriesByTag(tagId); + } + + return filteredRepos; + }, [repositories, languageId, tagId, filterRepositoriesByLanguage, filterRepositoriesByTag]); return (
diff --git a/components/Repository/SearchBar.tsx b/components/Repository/SearchBar.tsx index a0c46894..c0ac480b 100644 --- a/components/Repository/SearchBar.tsx +++ b/components/Repository/SearchBar.tsx @@ -1,17 +1,22 @@ // searchbar.tsx +"use client"; + import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { useAppData } from "../../hooks/useAppData"; export const SearchBar = () => { const [query, setQuery] = useState(""); const { filterRepositoriesByQuery } = useAppData(); - const handleSearch = (searchQuery: string) => { - setQuery(searchQuery); - filterRepositoriesByQuery(searchQuery); - }; + const handleSearch = useCallback( + (searchQuery: string) => { + setQuery(searchQuery); + filterRepositoriesByQuery(searchQuery); + }, + [filterRepositoriesByQuery] + ); return (
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index e83fe12e..6424dca2 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -4,7 +4,7 @@ import { faGithub } from "@fortawesome/free-brands-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useParams, usePathname } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useAppData } from "../hooks/useAppData"; import { AboutSection } from "./AboutSection"; import { LinkButton } from "./Button/LinkButton"; @@ -19,7 +19,10 @@ export const Sidebar = () => { const { languages, tags } = useAppData(); - // State variable to track whether the user has scrolled to a minimum height of 702 pixels vertically. + // Memoize languages and tags to avoid unnecessary re-renders + const memoizedLanguages = useMemo(() => languages, [languages]); + const memoizedTags = useMemo(() => tags, [tags]); + const [scrollHeightReached, setScrollHeightReached] = useState(false); const [showUpArrow, setShowUpArrow] = useState(false); @@ -30,20 +33,15 @@ export const Sidebar = () => { scrollTarget?.scrollIntoView({ behavior: "smooth" }); } - // Handle scroll events and set "scrollHeightReached" & "showUpArrow" to True, when the user scrolls to 702 pixels vertically. const handleScroll = () => { setScrollHeightReached(window.scrollY >= 702); setShowUpArrow(window.scrollY > 702); }; - // Add the scroll event listener window.addEventListener("scroll", handleScroll); - - // Remove the scroll event listener when the component unmounts return () => window.removeEventListener("scroll", handleScroll); }, [pageType]); - // Function to scroll to the top of the page const handleScrollToTop = () => window.scrollTo({ top: 0, behavior: "smooth" }); return ( @@ -74,11 +72,11 @@ export const Sidebar = () => { }`} > - +
{showUpArrow && }