为何组件仅需渲染一次,useEffect却多次执行?
问题解决:useEffect重复执行与筛选逻辑优化
问题现象
页面加载时控制台多次打印filtering events(每次加载执行三次),即便filters状态未更新,该useEffect仍会触发。需求是仅当filters更新时,基于筛选条件更新filteredEvents并刷新日历组件。
核心原因
- 内部组件重复创建:
UserInterface等组件定义在CourseSignup内部,每次父组件渲染都会重新创建这些组件,导致内部状态反复初始化,触发多次渲染与useEffect执行。 - 依赖数组不完整:负责筛选的useEffect仅监听
filters,未监听allEvents,导致事件数据加载完成后无法自动执行筛选;同时filters初始值设置不当(classTeacher为字符串'undefined'而非undefined),触发不必要的判断逻辑。 - 不必要的状态更新:无论筛选条件如何,都直接调用
setFilteredEvents(allEvents),既浪费性能,也会触发组件额外渲染。
解决方案
1. 重构组件结构,将内部组件移到外部
把UserInterface、FilterOptions、Calendar、SignupForm移到CourseSignup组件外部,避免父组件渲染时重复创建子组件,减少不必要的重渲染。
2. 修正useEffect依赖与筛选逻辑
- 给筛选逻辑添加
allEvents依赖,确保事件数据加载完成后自动执行筛选 - 修正
filters初始值,将classTeacher改为undefined - 优化筛选逻辑,仅在
allEvents.events存在时执行筛选,避免空值报错
3. 用useMemo替代useEffect+state(可选优化)
直接通过useMemo计算filteredEvents,无需额外维护filteredEvents状态,更符合React状态推导的设计原则。
修改后的完整代码
import React, { useState, useEffect, useMemo } from 'react' import { Calendar as BigCalendar, momentLocalizer } from 'react-big-calendar'; import moment from 'moment'; import loadingIMG from "../../../assets/images/courses/signup/minionsLoading.gif" import "react-big-calendar/lib/css/react-big-calendar.css" import { Accordion, AccordionItem, AccordionItemHeading, AccordionItemPanel, AccordionItemButton } from 'react-accessible-accordion' import { navigate, Link } from 'gatsby'; moment.locale('en-US') const localizer = momentLocalizer(moment) // 移到外部的子组件 const FilterOptions = () => { return null } const Calendar = ({ events }) => { const getDate = () => { const d = new Date() return `${d.toLocaleString('default', { month: 'long' })}, ${d.getFullYear()}` } return ( <div id="calendar-content"> <div className="section-title" style={{ marginBottom: "10px" }}> <h3 style={{ fontFamily: "inherit" }}>{getDate()}</h3> </div> <BigCalendar localizer={localizer} events={events} startAccessor="start" endAccessor="end" style={{ height: 800 }} views={{month: true}} popup={true} toolbar={false} /> </div> ) } const SignupForm = () => { return null } const UserInterface = ({ loading, allEvents, className }) => { const [filters, updateFilters] = useState({ classTeacher: undefined, // 修正为undefined而非字符串 classType: undefined, classFormat: undefined, class: className }) // 用useMemo推导filteredEvents,替代useEffect+state const filteredEvents = useMemo(() => { if (!allEvents?.events) return { count: 0, events: [] } console.log("filtering events") // 判断是否有有效筛选条件 const hasActiveFilters = Object.values(filters).some(val => val !== undefined) if (!hasActiveFilters) { return allEvents } // 执行实际筛选逻辑(替换你注释掉的代码) const filtered = allEvents.events.filter(event => { const matchesFormat = !filters.classFormat || event.info.format?.includes(filters.classFormat) const matchesType = !filters.classType || event.info.type?.includes(filters.classType) const matchesClass = !filters.class || event.info.name?.includes(filters.class) const matchesTeacher = !filters.classTeacher || event.info.teacher?.includes(filters.classTeacher) return matchesFormat && matchesType && matchesClass && matchesTeacher }) return { count: filtered.length, events: filtered } }, [filters, allEvents]) // 依赖filters和allEvents return ( <div> <FilterOptions/> <div> {loading ? null : <Calendar events={filteredEvents.events}/>} </div> <SignupForm/> </div> ) } const CourseSignup = ({className}) => { const [loading, isLoading] = useState(true) const [allEvents, setAllEvents] = useState({ count: undefined, events: undefined }) useEffect(() => { // 注意:useEffect不要用async直接作为回调,避免内存泄漏 const fetchEvents = async () => { console.log("fetching events") try { const response = await fetch("http://127.0.0.1:5000/timeslots", { method: 'GET', headers: { 'X-API-Key': '*F-JaNdRgUkXp2s5v8x/A?D(G+KbPeSh', } }) const results = await response.json() const temp = results.events.map(event => ({ title: String(event.summary), info: { format: undefined, type: undefined, name: undefined, teacher: undefined }, description: String(event.description) || undefined, start: new Date(event.start.date || event.start.dateTime), end: new Date(event.end.date || event.end.dateTime), url: String(event.htmlLink), id: String(event.id), est_locale_timecodes: { start: event.start.date || event.start.dateTime, end: event.end.date || event.end.dateTime } })) setAllEvents({ count: temp.length, events: temp }) } catch (err) { setAllEvents({ count: undefined, events: undefined }) } finally { isLoading(false) } } fetchEvents() }, []) return ( <UserInterface loading={loading} allEvents={allEvents} className={className}/> ) } export default CourseSignup
额外说明
- 原代码中
useEffect直接使用async作为回调不规范,会导致内存泄漏,已修正为内部定义async函数并调用 - 用
map替代for循环处理事件数组,代码更简洁 - 筛选逻辑中添加了可选链操作符(
?.),避免空值访问报错
内容的提问来源于stack exchange,提问作者Sriman Achanta
相关产品推荐
相关产品推荐

