Reputation: 4843
I've the following component:
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;
The <Undo/>
component is a buffer storing the value of an input on every change, allowing to perform an undo action.
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>
);
}
Example:
If a user types 'abc' on the input, buffer = ['a', 'ab', 'abc']
and index = 2
.
When a user clicks on the button, the component should go back to the previous state. That means, buffer = ['a', 'ab', 'abc']
and index = 1
.
After clicking on the button, setName(buffer[pos])
is executed, the useEffect()
is retriggered: buffer = ['ab', 'abc', 'ab']
and index = 2
(not desired).
How can I prevent to retrigger the useEffect()
Hook, iff it was retriggered by the undo()
function?
Upvotes: 1
Views: 709
Reputation: 1787
I wouldn't use useEffect for this. Your undo buffer is not, in my humble opinion, a side-effect in the React DOM sense, but rather internal logic you want to apply on state change. Likewise, your Undo component is a button that also happens to have reusable logic about history and state that arguably belongs more to the parent than the button itself.
Overall, I think this approach is not a clean top-down flow, and tends to lead to this kind of problems in React.
I would instead start by putting your undo logic and state in the parent:
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}
After wiring up everything, you'll probably feel that you lost the clean reusability of your Undo component by doing that and you'd be right. But this is exactly what Custom Hooks are for: reusable stateful logic. There are many ways ways to go about it, but here's an example of a custom hook you could write:
const [name, setName, undoName] = useUndoableState('');
The hook would replace useState (and use it itself inside the hook), manage the buffer, and provide an additional function to call undo that rewinds and set the new name.
<input onChange={setName} />
<button onClick={undoName}>Undo</button>
Upvotes: 3