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

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;

问题根源与修复说明

核心问题点

  1. useEffect依赖循环:原代码中useEffect依赖journalID,但内部又调用updateJournalID修改该状态,导致每次更新都触发useEffect,形成循环执行。
  2. localStorage读取时机错误:组件挂载后在useEffect中读取localStorage并设置状态,额外触发一次useEffect执行。
  3. 清理函数闭包问题:异步清理函数中引用的journalID是旧值,导致逻辑执行错误。
  4. useCallback依赖冗余:captureSelectedBlocks依赖自身修改的状态,导致每次渲染都重新创建函数,引发不必要的组件更新。

关键修复动作

  1. 初始化同步localStorage:直接在useState初始化时读取localStorage,避免额外状态更新。
  2. 标记首次挂载:用useRef标记首次执行,避免重复触发初始化逻辑。
  3. 清理函数用ref保存当前值:解决闭包导致的旧值问题,确保清理逻辑拿到最新的journalID。
  4. 优化useCallback依赖:改用函数式更新状态,移除不必要的依赖,避免函数重复创建。

内容的提问来源于stack exchange,提问作者Sushant Kadam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 03:57:50