首页 > 解决方案 > 从特定函数更新状态时防止 useEffect 重新触发

问题描述

我有以下组件:

import React, { useEffect, useState } from 'react';

const UndoInput = () => {
    
    const [name, setName] = useState('');
    
    return(
        <div className = 'Name'>
            <input  
                onChange = {(e) => setName(e.target.value)} 
                value    = {name}>
            </input>
            <Undo
                name     = {name}
                setName  = {setName}
            />
        </div>
    );
    
}

export default UndoInput;

<Undo/>组件是一个缓冲区,用于存储每次更改时输入的值,允许执行撤消操作。

const Undo = ({name, setName}) => {
    
    const [buffer, setBuffer] = useState(['', '', '']);
    const [index,  setIndex]  = useState(-1);
    
    useEffect(() => {
        
        let copy = [...buffer, name].slice(-3);
        
        let pos = index + 1;
        
        if(pos > 2)
            pos = 2;
        
        setBuffer(copy);
        setIndex(pos);
        
    }, [name]);
    
    const undo = () => {
        
        let pos = index - 1;
        
        if(pos < 0)
            pos = -1;
        
        setName(buffer[pos]);
        setIndex(pos);
        
    }
    
    return(
        <button onClick = {undo}>Undo</button>
    );
    
}

例子:

如果用户在输入中键入“abc”,buffer = ['a', 'ab', 'abc']并且index = 2.

当用户点击按钮时,组件应该回到之前的状态。这意味着,buffer = ['a', 'ab', 'abc']index = 1

单击按钮后,setName(buffer[pos])执行,重新useEffect()触发:buffer = ['ab', 'abc', 'ab']index = 2(不需要)。

useEffect()如果钩子被函数重新触发,我如何防止重新触发钩子undo()

标签: reactjsreact-hooks

解决方案


我不会为此使用 useEffect 。在我看来,您的撤消缓冲区不是 React DOM 意义上的副作用,而是您想要应用于状态更改的内部逻辑。同样,您的 Undo 组件是一个按钮,它也恰好具有关于历史和状态的可重用逻辑,可以说它更多地属于父级而不是按钮本身。

总的来说,我认为这种方法不是一个干净的自上而下的流程,并且容易在 React 中导致这种问题。

相反,我会先将您的撤消逻辑和状态放在父级中:

const handleChange = function(e){
   // add to undo buffer here
   setName(e.target.value);
}
const handleUndo = function(){
   // fetch your undo buffer here
   setName(undoValue);
}
onChange = {handleChange} 
onClick = {handleUndo} 

在连接完所有内容之后,您可能会觉得这样做失去了 Undo 组件的干净可重用性,而且您是对的。但这正是Custom Hooks的用途:可重用的有状态逻辑。有很多方法可以解决它,但这里有一个您可以编写的自定义钩子的示例:

const [name, setName, undoName] = useUndoableState('');

钩子将替换 useState(并在钩子内部使用它自己),管理缓冲区,并提供一个额外的函数来调用撤销并设置新名称。

<input onChange={setName} />
<button onClick={undoName}>Undo</button>

推荐阅读