You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何组件仅需渲染一次,useEffect却多次执行?

问题解决:useEffect重复执行与筛选逻辑优化

问题现象

页面加载时控制台多次打印filtering events(每次加载执行三次),即便filters状态未更新,该useEffect仍会触发。需求是仅当filters更新时,基于筛选条件更新filteredEvents并刷新日历组件。

核心原因

  1. 内部组件重复创建:UserInterface等组件定义在CourseSignup内部,每次父组件渲染都会重新创建这些组件,导致内部状态反复初始化,触发多次渲染与useEffect执行。
  2. 依赖数组不完整:负责筛选的useEffect仅监听filters,未监听allEvents,导致事件数据加载完成后无法自动执行筛选;同时filters初始值设置不当(classTeacher为字符串'undefined'而非undefined),触发不必要的判断逻辑。
  3. 不必要的状态更新:无论筛选条件如何,都直接调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 20:09:22