diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..b901576 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-06 - Component Optimizations +**Learning:** Re-renders can be expensive if static arrays (e.g., config arrays) are defined inside the component body, as they break referential equality for `useMemo` dependencies and cause recalculations on every render. +**Action:** Always move static arrays and helper functions outside the component body when possible, and consistently wrap derived arrays (like client-side filtered lists) with `useMemo` to prevent expensive recalculations and unnecessary re-renders. diff --git a/src/pages/Documentaries.jsx b/src/pages/Documentaries.jsx index 8befef4..eba2709 100644 --- a/src/pages/Documentaries.jsx +++ b/src/pages/Documentaries.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useMemo } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBook, @@ -26,6 +26,27 @@ import ErrorMessage from "../components/common/ErrorMessage"; import SkeletonLoader from "../components/common/SkeletonLoader"; import Button from "../components/common/Button"; +const categories = [ + { id: "all", name: "All Resources", icon: faBook }, + { id: "html", name: "HTML", icon: faHtml5, color: "text-orange-600" }, + { id: "css", name: "CSS", icon: faCss3Alt, color: "text-blue-600" }, + { + id: "javascript", + name: "JavaScript", + icon: faJsSquare, + color: "text-yellow-600", + }, + { id: "python", name: "Python", icon: faPython, color: "text-green-600" }, + { id: "react", name: "React", icon: faReact, color: "text-cyan-600" }, + { id: "node", name: "Node.js", icon: faNodeJs, color: "text-green-500" }, + { + id: "database", + name: "Database", + icon: faDatabase, + color: "text-purple-600", + }, +]; + const Documentaries = () => { const [searchTerm, setSearchTerm] = useState(""); const [selectedCategory, setSelectedCategory] = useState("all"); @@ -37,27 +58,6 @@ const Documentaries = () => { refetch, } = useApi(documentariesApi.getAll); - const categories = [ - { id: "all", name: "All Resources", icon: faBook }, - { id: "html", name: "HTML", icon: faHtml5, color: "text-orange-600" }, - { id: "css", name: "CSS", icon: faCss3Alt, color: "text-blue-600" }, - { - id: "javascript", - name: "JavaScript", - icon: faJsSquare, - color: "text-yellow-600", - }, - { id: "python", name: "Python", icon: faPython, color: "text-green-600" }, - { id: "react", name: "React", icon: faReact, color: "text-cyan-600" }, - { id: "node", name: "Node.js", icon: faNodeJs, color: "text-green-500" }, - { - id: "database", - name: "Database", - icon: faDatabase, - color: "text-purple-600", - }, - ]; - const externalResources = [ { id: 1, @@ -149,17 +149,19 @@ const Documentaries = () => { }, ]; - const filteredResources = externalResources.filter((resource) => { - const matchesSearch = - !searchTerm || - resource.title.toLowerCase().includes(searchTerm.toLowerCase()) || - resource.description.toLowerCase().includes(searchTerm.toLowerCase()); + const filteredResources = useMemo(() => { + return externalResources.filter((resource) => { + const matchesSearch = + !searchTerm || + resource.title.toLowerCase().includes(searchTerm.toLowerCase()) || + resource.description.toLowerCase().includes(searchTerm.toLowerCase()); - const matchesCategory = - selectedCategory === "all" || resource.category === selectedCategory; + const matchesCategory = + selectedCategory === "all" || resource.category === selectedCategory; - return matchesSearch && matchesCategory; - }); + return matchesSearch && matchesCategory; + }); + }, [searchTerm, selectedCategory]); const getDifficultyColor = (difficulty) => { switch (difficulty) { diff --git a/src/pages/Forum.jsx b/src/pages/Forum.jsx index 1c15a08..5e6fbec 100644 --- a/src/pages/Forum.jsx +++ b/src/pages/Forum.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { @@ -27,6 +27,71 @@ import SkeletonLoader from '../components/common/SkeletonLoader'; import Button from '../components/common/Button'; import { formatDate, truncateText } from '../utils/helpers'; +const communityLinks = [ + { + name: 'Telegram Group', + icon: faTelegram, + url: 'https://t.me/example', + members: '2.5K+', + description: 'Chat real-time dengan developer lainnya', + color: 'bg-blue-500' + }, + { + name: 'Discord Server', + icon: faDiscord, + url: 'https://discord.gg/example', + members: '1.8K+', + description: 'Voice chat dan screen sharing untuk coding session', + color: 'bg-indigo-500' + }, + { + name: 'WhatsApp Group', + icon: faWhatsapp, + url: 'https://chat.whatsapp.com/example', + members: '500+', + description: 'Diskusi ringan dan sharing tips programming', + color: 'bg-green-500' + } +]; + +const recentTopics = [ + { + id: 1, + title: 'Cara deploy aplikasi React ke Vercel?', + author: 'Andi Developer', + replies: 12, + views: 156, + lastActivity: '2 jam lalu', + category: 'React' + }, + { + id: 2, + title: 'Best practice untuk REST API dengan Node.js', + author: 'Sari Backend', + replies: 8, + views: 89, + lastActivity: '4 jam lalu', + category: 'Backend' + }, + { + id: 3, + title: 'Tutorial CSS Grid vs Flexbox', + author: 'Budi Frontend', + replies: 15, + views: 234, + lastActivity: '1 hari lalu', + category: 'CSS' + } +]; + +const categories = [ + { id: 'all', name: 'Semua Diskusi', icon: faComments }, + { id: 'web', name: 'Web Development', icon: faRocket }, + { id: 'data', name: 'Data Science', icon: faQuestionCircle }, + { id: 'frontend', name: 'Frontend', icon: faUsers }, + { id: 'backend', name: 'Backend', icon: faUsers } +]; + const Forum = () => { const [selectedCategory, setSelectedCategory] = useState('all'); const [searchTerm, setSearchTerm] = useState(''); @@ -34,81 +99,18 @@ const Forum = () => { const { data: forums, loading: forumsLoading, error: forumsError } = useApi(forumsApi.getAll); const { data: posts, loading: postsLoading, error: postsError } = useApi(postsApi.getAll); - const communityLinks = [ - { - name: 'Telegram Group', - icon: faTelegram, - url: 'https://t.me/example', - members: '2.5K+', - description: 'Chat real-time dengan developer lainnya', - color: 'bg-blue-500' - }, - { - name: 'Discord Server', - icon: faDiscord, - url: 'https://discord.gg/example', - members: '1.8K+', - description: 'Voice chat dan screen sharing untuk coding session', - color: 'bg-indigo-500' - }, - { - name: 'WhatsApp Group', - icon: faWhatsapp, - url: 'https://chat.whatsapp.com/example', - members: '500+', - description: 'Diskusi ringan dan sharing tips programming', - color: 'bg-green-500' - } - ]; - - const recentTopics = [ - { - id: 1, - title: 'Cara deploy aplikasi React ke Vercel?', - author: 'Andi Developer', - replies: 12, - views: 156, - lastActivity: '2 jam lalu', - category: 'React' - }, - { - id: 2, - title: 'Best practice untuk REST API dengan Node.js', - author: 'Sari Backend', - replies: 8, - views: 89, - lastActivity: '4 jam lalu', - category: 'Backend' - }, - { - id: 3, - title: 'Tutorial CSS Grid vs Flexbox', - author: 'Budi Frontend', - replies: 15, - views: 234, - lastActivity: '1 hari lalu', - category: 'CSS' - } - ]; + const filteredPosts = useMemo(() => { + return posts?.filter(post => { + const matchesSearch = !searchTerm || + post.title.toLowerCase().includes(searchTerm.toLowerCase()) || + post.content.toLowerCase().includes(searchTerm.toLowerCase()); - const filteredPosts = posts?.filter(post => { - const matchesSearch = !searchTerm || - post.title.toLowerCase().includes(searchTerm.toLowerCase()) || - post.content.toLowerCase().includes(searchTerm.toLowerCase()); - - const matchesCategory = selectedCategory === 'all' || - (forums && forums.some(forum => forum.id === post.forum_id && forum.title.toLowerCase().includes(selectedCategory))); - - return matchesSearch && matchesCategory; - }) || []; + const matchesCategory = selectedCategory === 'all' || + (forums && forums.some(forum => forum.id === post.forum_id && forum.title.toLowerCase().includes(selectedCategory))); - const categories = [ - { id: 'all', name: 'Semua Diskusi', icon: faComments }, - { id: 'web', name: 'Web Development', icon: faRocket }, - { id: 'data', name: 'Data Science', icon: faQuestionCircle }, - { id: 'frontend', name: 'Frontend', icon: faUsers }, - { id: 'backend', name: 'Backend', icon: faUsers } - ]; + return matchesSearch && matchesCategory; + }) || []; + }, [posts, searchTerm, selectedCategory, forums]); return (