React组件useEffect多次执行致日志条目重复创建删除求助
React组件useEffect重复执行导致日志条目重复创建/删除问题解决
问题描述
我在React组件里碰到个问题:useEffect钩子会执行8-9次,导致日志条目被重复创建和删除。试过给useEffect里的函数用useCallback,但引发了无限循环,不知道问题出在哪,也不确定清理函数用得对不对。
已尝试但无效的方案
- 为useEffect内的函数使用useCallback
- 移除清理函数排查根因
- 避免从localStorage更新状态
- 多次修改useEffect函数
组件代码
import Editor from '../components/text-editor/Editor'; import Layout from '../containers/Layout'; import { useUser } from '../contexts/UserContext'; import { getJournalEntryById, updateJournalEntry, createJournalEntry, deleteJournalEntry, } from '../services/journalEntryService'; import { useParams } from 'react-router-dom'; import { useMenu } from '../contexts/NavDrawerContext'; import { useState, useEffect, useRef, useCallback } from 'react'; const Journal = () => { const { userID, toggleDarkMode } = useUser(); const [initialContent, setInitialContent] = useState([]); const [selectedBlocks, setSelectedBlocks] = useState([]); const [isSelectionActive, setIsSelectionActive] = useState(false); const [isFocused, setIsFocused] = useState(false); const [isPromptDisplayVisible, setIsPromptDisplayVisible] = useState(false); const [currentMood, setCurrentMood] = useState('happy'); const [isFocusModeOn, setIsFocusModeOn] = useState(false); const [journalID, setJournalID] = useState(() => localStorage.getItem('journalID') || ''); const [isTextEditorMenuCollapsed, setIsTextEditorMenuCollapsed] = useState(false); const containerRef = useRef(null); // Function to scroll to the bottom of the editor const scrollToBottom = () => { if (containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } }; const updateJournalID = (id) => { if (id !== journalID) { setJournalID(id); localStorage.setItem('journalID', id); } }; const resetJournalID = () => { setJournalID(''); localStorage.removeItem('journalID'); }; const isFirstMount = useRef(true); useEffect(() => { async function fetchExistingJournal(id) { try { const journalEntry = await getJournalEntryById(id); if (journalEntry) { const { content, mood } = journalEntry; const blocks = JSON.parse(content); setInitialContent(blocks); setCurrentMood(mood); } } catch (error) { console.error('Error fetching journal entry:', error); resetJournalID(); } } async function createNewJournal() { try { const response = await createJournalEntry(initialContent, currentMood); updateJournalID(response._id); } catch (error) { console.error('Error creating journal entry:', error); resetJournalID(); } } // 首次挂载逻辑 if (isFirstMount.current) { isFirstMount.current = false; journalID ? fetchExistingJournal(journalID) : createNewJournal(); return; } // 后续仅当journalID有效时执行 if (journalID) { fetchExistingJournal(journalID); } // 清理函数:用ref保存当前journalID避免闭包问题 const currentJournalID = useRef(journalID); currentJournalID.current = journalID; return async () => { try { const id = currentJournalID.current; if (!id) return; const response = await getJournalEntryById(id); const content = JSON.parse(response.content); if (content.length === 0) { await deleteJournalEntry(id); console.log('Journal entry deleted'); } } catch (error) { console.error('Error deleting journal entry if empty:', error); } }; }, [journalID]); const handleJournalUpdate = useCallback( async (editor) => { const newContent = editor.topLevelBlocks; captureSelectedBlocks(editor); scrollToBottom(); if (newContent?.length && journalID?.length) { try { await updateJournalEntry(journalID, 'content', newContent); } catch (error) { console.error('Failed to update journal entry:', error); } } }, [journalID] ); const handleMoodChange = (mood) => { setCurrentMood(mood); }; // Function to reset journal from navigation const resetJournal = () => { setInitialContent([]); resetJournalID(); setCurrentMood('happy'); }; // Function to capture selected blocks const captureSelectedBlocks = useCallback( (editor) => { const currentSelectedBlocks = editor.getSelection()?.blocks; const currentActiveBlock = editor.getTextCursorPosition().block; if (currentSelectedBlocks) { setSelectedBlocks(currentSelectedBlocks); setIsSelectionActive(true); } else { setIsSelectionActive(prev => prev ? false : prev); setSelectedBlocks([currentActiveBlock]); } }, [] ); return ( <Layout currentMood={currentMood} handleMoodChange={handleMoodChange} isFocusModeOn={isFocusModeOn} setIsFocusModeOn={setIsFocusModeOn} setIsPromptDisplayVisible={setIsPromptDisplayVisible} setIsTextEditorMenuCollapsed={setIsTextEditorMenuCollapsed} toggleDarkMode={toggleDarkMode} showFocusModeAndMoodDropdown={true} handleJournalUpdate={handleJournalUpdate} journalID={journalID} resetJournal={resetJournal} > <Editor initialContent={initialContent} userID={userID} journalID={journalID} handleJournalUpdate={handleJournalUpdate} selectedBlocks={selectedBlocks} setSelectedBlocks={setSelectedBlocks} captureSelectedBlocks={captureSelectedBlocks} currentMood={currentMood} setCurrentMood={setCurrentMood} isTextEditorMenuCollapsed={isTextEditorMenuCollapsed} setIsTextEditorMenuCollapsed={setIsTextEditorMenuCollapsed} isFocusModeOn={isFocusModeOn} setIsFocusModeOn={setIsFocusModeOn} isPromptDisplayVisible={isPromptDisplayVisible} setIsPromptDisplayVisible={setIsPromptDisplayVisible} isFocused={isFocused} setIsFocused={setIsFocused} containerRef={containerRef} isSelectionActive={isSelectionActive} setIsSelectionActive={setIsSelectionActive} /> </Layout> ); }; export default Journal;
问题根源与修复说明
核心问题点
- useEffect依赖循环:原代码中useEffect依赖
journalID,但内部又调用updateJournalID修改该状态,导致每次更新都触发useEffect,形成循环执行。 - localStorage读取时机错误:组件挂载后在useEffect中读取localStorage并设置状态,额外触发一次useEffect执行。
- 清理函数闭包问题:异步清理函数中引用的
journalID是旧值,导致逻辑执行错误。 - useCallback依赖冗余:
captureSelectedBlocks依赖自身修改的状态,导致每次渲染都重新创建函数,引发不必要的组件更新。
关键修复动作
- 初始化同步localStorage:直接在useState初始化时读取localStorage,避免额外状态更新。
- 标记首次挂载:用
useRef标记首次执行,避免重复触发初始化逻辑。 - 清理函数用ref保存当前值:解决闭包导致的旧值问题,确保清理逻辑拿到最新的
journalID。 - 优化useCallback依赖:改用函数式更新状态,移除不必要的依赖,避免函数重复创建。
内容的提问来源于stack exchange,提问作者Sushant Kadam
相关产品推荐
相关产品推荐

