Create the setTimeout in the parent and closes it in the children - reactjs

I have a specific problem that is keeping me awake this whole week.
I have a parent component which has a pop-up children component. When I open the page the pop-up shows off and after 5 seconds it disappears with a setTimeout.
This pop-up has an input element in it.
I want the pop-up to disappear after 5 seconds or if I click to digit something in the input. I tried to create a timerRef to the setTimeout and closes it in the children but it didn't work.
Can you help me, please? Thanks in advance.
ParentComponent.tsx
const ParentComponent = () => {
const [isVisible, setIsVisible] = useState(true)
timerRef = useRef<ReturnType<typeof setTimeout>>()
timerRef.current = setTimeout(() => {
setIsVisible(false)
}, 5000)
useEffect(() => {
return () => clearTimeout()
})
return (
<div>
<ChildrenComponent isVisible={isVisible} inputRef={timerRef} />
</div>
)
}
ChildrenComponent.tsx
const ChildrenComponent = ({ isVisible, inputRef}) => {
return (
<div className=`${isVisible ? 'display-block' : 'display-none'}`>
<form>
<input onClick={() => clearTimeout(inputRef.current as NodeJS.Timeout)} />
</form>
</div>
)
}

You're setting a new timer every time the the component re-renders, aka when the state changes which happens in the timeout itself.
timerRef.current = setTimeout(() => {
setIsVisible(false);
}, 5000);
Instead you can put the initialization in a useEffect.
useEffect(() => {
if (timerRef.current) return;
timerRef.current = setTimeout(() => {
setIsVisible(false);
}, 5000);
return () => clearTimeout(timerRef.current);
}, []);
You should also remove the "loose" useEffect that runs on every render, this one
useEffect(() => {
return () => clearTimeout();
});

Related

Click event added in useEffect after changing state by onClick is executed in same moment

React 18 changed useEffect timing at it broke my code, that looks like this:
const ContextualMenu = ({ isDisabled }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleMenu = useCallback(
() => {
if (isDisabled) return;
setIsExpanded((prevState) => !prevState);
},
[isDisabled],
);
useEffect(() => {
if (isExpanded) {
window.document.addEventListener('click', toggleMenu, false);
}
return () => {
window.document.removeEventListener('click', toggleMenu, false);
};
}, [isExpanded]);
return (
<div>
<div class="button" onClick={toggleMenu}>
<Icon name="options" />
</div>
{isExpanded && <ListMenu />}
</div>
);
};
The problem is, toggleMenu function is executed twice on button click - first one is correct, it's onClick button action, which changes state, but this state change executes useEffect (which adds event listener on click) and this click is executed on the same click, that triggered state change.
So, what should be correct and most "in reactjs spirit" way to fix this?
Your problem is named Event bubbling
You can use stopPropagation to fix that
const toggleMenu = useCallback(
(event) => {
event.stopPropagation();
if (isDisabled) return;
setIsExpanded((prevState) => !prevState);
},
[isDisabled],
);

clickOutside hook doesn't work on 2nd click

I have a list of items and want to include an icon that opens a modal for a user to choose 'edit' or 'delete' the item.
And I put this code inside the ActionModal so that only clicked modal would open by comparing the ids.
The problem is, clicking outside the element work only one time and after that, nothing happens when the ellipsis button clicked. I think it's probably because the state inside ActionModal, 'modalOpen' remains false, but I'm stuck here and don't know how to handle it.
if (!isOpen.show || isOpen.id !== id || !modalOpen) return null;
const List = () => {
const [modal, setModal] = useState({ id: null, show: false });
const onDialogClick = (e) => {
setModal((prevState) => {
return { id: e.target.id, show: !prevState.show };
});
};
const journals = journals.map((journal) => (
<StyledList key={journal.id}>
<Option>
<FontAwesomeIcon
icon={faEllipsisV}
id={journal.id}
onClick={onDialogClick}
/>
<ActionModal
actions={['edit', 'delete']}
id={journal.id}
isOpen={modal}
></ActionModal>
</Option>
const ActionModal = ({ id, actions, isOpen }) => {
const content = actions.map((action) => <li key={action}>{action}</li>);
const ref = useRef();
const [modalOpen, setModalOpen] = useState(true);
useOnClickOutside(ref, () => setModalOpen(!modalOpen));
if (!isOpen.show || isOpen.id !== id || !modalOpen) return null;
return (
<StyledDiv>
<ul ref={ref}>{content}</ul>
</StyledDiv>
);
};
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
So fistly you do not need to include ref and handler as dependencies in useEffect hook, because evvent listeners are set on initial load, there is no need to set it on every value change.
I think I don't fully understand your situation. So you need to close the modal after it is opened by pressing outside it? or you want to be able to press that 3 dots icon when it's opened?
P.S.
I little bit condesed your code. Try this and let me know what is happening. :)
const ActionModal = ({ id, actions, isOpen, setOpen }) => {
const ref = useRef();
const content = actions.map((action) => <li key={action}>{action}</li>);
useEffect(() => {
const listener = (event) => {
if (ref.current || !ref.current.contains(event.target)) {
setOpen({...open, show: false});
}
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, []);
return (
<StyledDiv>
<div ref={ref}>
<ul>{content}</ul>
</div>
</StyledDiv>
);
};

clear timeout in react if component state changes

I have code using react hooks that is fetching some data from an API after a search value has been entered.
I am trying to activate a Timeout that will stop in case the value of const [searchResults, setSearchResults] = useState([]);changes.
I am not able to find out the way to do it, I can activate it when the use effect for fetching the information is activated, but I can not deactivate it in case new information is found.
In a nutshell:
I would like to know how to clearTimeout in case searchResults has value.
My code looks like:
useEffect(() => {
setIsLoading(true);
ProductsApi.getSearchProducts(query,setSearchResults, setIsLoading);
setTimeout(() => {
setError(false)
}, 2000);
}, [query]);
useEffect(() => {
clearTimeout(timer);
}, [searchResults]);
A generic answer to your problem
const MyComponent = () => {
const [shouldRun, setShouldRun] = useState(true);
useEffect(() => {
const TIMEOUT_DURATION = 10000;
const myTimeoutCallback = () => {
if (!shouldRun) return;
// Add your logic here
console.log("My callback was invoked after a delay.");
};
const timeoutId = setTimeout(myTimeoutCallback, TIMEOUT_DURATION);
return () => clearTimeout(timeoutId);
}, [shouldRun]);
};
You can use shouldRun as a condition if the callback function should be invoked or not.
To clear the time out you can do this: const timer = setTimeout(() => {.
However with your code, timer will be out of scope when you call clearTimeout(timer).
You should adjust your code so that timer is in scope allowing access to the variable timer.
Use setInterval
function DemoApp() {
React.useEffect(() => {
const timer = window.setInterval(() => {
console.log('2 seconds has passed');
}, 2000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>
My Demo Timer | Please check console.
</div>
);
}
ReactDOM.render(
<div>
<DemoApp />
</div>,
document.querySelector("#DemoApp")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="DemoApp"></div>

setInterval does not work properly in ReactJS

I got a problem with setInterval in ReactJS (particularly Functional Component). I want to enable autoplay slider (it means value in Slider Bar will be increased after a period of time). This function will be triggered when I click a button. But I don't know exactly how setInterval works with setValue. The console log just returns the initial value (does not change) (console.log function was called in a callback function of setInterval).
My code is below:
const [value, setValue] = useState(10);
const [playable, setPlayable] = useState(false);
useEffect(() => {
if (playable === true) {
console.log("Trigger");
const intervalId = setInterval(increaseValue, 1000);
return () => clearInterval(intervalId);
}
}, [playable]);
const increaseValue = () => {
console.log(value);
setValue(value + 1);
};
const autoPlay = () => {
setPlayable(true);
};
return (
<div>
<div style={{"width": "800px"}}>
<Slider
value={value}
step={1}
valueLabelDisplay="on"
getAriaValueText={valueText}
marks={marks}
onChange={handleChange}
min={0}
max={100}
/>
</div>
<button onClick={autoPlay}>Play</button>
</div>
)
So, I think you need to pass increaseValue function into useEffect
useEffect(() => {
if (playable === true) {
console.log("Trigger");
const intervalId = setInterval(increaseValue, 1000);
return () => clearInterval(intervalId);
}
}, [playable, increaseValue]);

how do I clearInterval on-click, with React Hooks?

I'm trying to refactor my code to react hooks, but I'm not sure if i'm doing it correctly. I tried copying and pasting my setInterval/setTimout code into hooks, but it did not work as intended. After trying different things I was able to get it to work, but I'm not sure if this is the best way to do it.
I know i can use useEffect to clear interval on un-mount, but I want to clear it before un-mounting.
Is the following good practice and if not what is a better way of clearing setInterval/setTimout before un-mounting?
Thanks,
useTimeout
import { useState, useEffect } from 'react';
let timer = null;
const useTimeout = () => {
const [count, setCount] = useState(0);
const [timerOn, setTimerOn] = useState(false);
useEffect(() => {
if (timerOn) {
console.log("timerOn ", timerOn);
timer = setInterval(() => {
setCount((prev) => prev + 1)
}, 1000);
} else {
console.log("timerOn ", timerOn);
clearInterval(timer);
setCount(0);
}
return () => {
clearInterval(timer);
}
}, [timerOn])
return [count, setCount, setTimerOn];
}
export default useTimeout;
Component
import React from 'react';
import useTimeout from './useTimeout';
const UseStateExample = () => {
const [count, setCount, setTimerOn] = useTimeout()
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
<br />
<button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
<br />
<button onClick={() => setTimerOn(true)}>Set Interval</button>
<br />
<button onClick={() => setTimerOn(false)}>Stop Interval</button>
<br />
</div>
);
}
export default UseStateExample;
--- added # 2019-02-11 15:58 ---
A good pattern to use setInterval with Hooks API:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
--- origin answer ---
Some issues:
Do not use non-constant variables in the global scope of any modules. If you use two instances of this module in one page, they’ll share those global variables.
There’s no need to clear timer in the “else” branch because if the timerOn change from true to false, the return function will be executed.
A better way in my thoughts:
import { useState, useEffect } from 'react';
export default (handler, interval) => {
const [intervalId, setIntervalId] = useState();
useEffect(() => {
const id = setInterval(handler, interval);
setIntervalId(id);
return () => clearInterval(id);
}, []);
return () => clearInterval(intervalId);
};
Running example here:
https://codesandbox.io/embed/52o442wq8l?codemirror=1
In this example, we add a couple of things...
A on/off switch for the timeout (the 'running' arg) which will completely switch it on or off
A reset function, allowing us to set the timeout back to 0 at any time:
If called while it's running, it'll keep running but return to 0.
If called while it's not running, it'll start it.
const useTimeout = (callback, delay, running = true) => {
// save id in a ref so we make sure we're always clearing the latest timeout
const timeoutId = useRef('');
// save callback as a ref so we can update the timeout callback without resetting it
const savedCallback = useRef();
useEffect(
() => {
savedCallback.current = callback;
},
[callback],
);
// clear the timeout and start a new one, updating the timeoutId ref
const reset = useCallback(
() => {
clearTimeout(timeoutId.current);
const id = setTimeout(savedCallback.current, delay);
timeoutId.current = id;
},
[delay],
);
// keep the timeout dynamic by resetting it whenever its' deps change
useEffect(
() => {
if (running && delay !== null) {
reset();
return () => clearTimeout(timeoutId.current);
}
},
[delay, running, reset],
);
return { reset };
};
So in your example above, we could use it like so...
const UseStateExample = ({delay}) => {
// count logic
const initCount = 0
const [count, setCount] = useState(initCount)
const incrementCount = () => setCount(prev => prev + 1)
const decrementCount = () => setCount(prev => prev - 1)
const resetCount = () => setCount(initCount)
// timer logic
const [timerOn, setTimerOn] = useState(false)
const {reset} = useTimeout(incrementCount, delay, timerOn)
const startTimer = () => setTimerOn(true)
const stopTimer = () => setTimerOn(false)
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={incrementCount}>Increment</button>
<br />
<button onClick={decrementCount}>Decrement</button>
<br />
<button onClick={startTimer}>Set Interval</button>
<br />
<button onClick={stopTimer}>Stop Interval</button>
<br />
<button onClick={reset}>Start Interval Again</button>
<br />
</div>
);
}
Demo of clear many timers.
You should declare and clear timer.current instead of timer.
Declare s and timer.
const [s, setS] = useState(0);
let timer = useRef<NodeJS.Timer>();
Initialize timer in useEffect(() => {}).
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
Clear timer.
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
After many attempts to make a timer work with setInterval, I decided to use setTimeOut, I hope it works for you.
const [count, setCount] = useState(60);
useEffect(() => {
if (count > 0) {
setTimeout(() => {
setCount(count - 1);
}, 1000);
}
}, [count]);

Resources