Updating React state in nested setTimeout callbacks - reactjs

Can someone please tell me what's wrong with this and why the state of the 'video variable' remains false? So, even after the h2 element has rendered and is visible (i.e. the state of the video variable has been updated to true), when I click and call the hideVideo function, the video state remains false? Many thanks.
export default function App() {
const [message, showMessage] = useState(false);
const [video, setVideo] = useState(false);
let modalTimeout, videoTimeout;
useEffect(() => {
window.addEventListener("click", hideVideo);
setupTimeouts();
return () => {
clearTimeout(modalTimeout);
clearTimeout(videoTimeout);
};
}, []);
const setupTimeouts = () => {
modalTimeout = setTimeout(() => {
showMessage(true);
videoTimeout = setTimeout(() => {
showMessage(false);
setVideo(true);
}, 4000);
}, 2000);
};
const hideVideo = () => {
console.log(video);
showMessage(false);
if (video === true) {
setVideo(false);
}
};
return (
<div className="App">
{message && <h1>Message</h1>}
{video && <h2>Video</h2>}
</div>
);
}

When you call useEffect the window listener attach the default video value that is false to the function hideVideo() so it will be always false, I created a button to show you that the video state value does change. check the last test function
export default function App() {
const [message, showMessage] = useState(false);
const [video, setVideo] = useState(false);
let modalTimeout, videoTimeout;
useEffect(() => {
window.addEventListener("click", hideVideo);
setupTimeouts();
return () => {
clearTimeout(modalTimeout);
clearTimeout(videoTimeout);
};
}, []);
const setupTimeouts = () => {
modalTimeout = setTimeout(() => {
showMessage(true);
videoTimeout = setTimeout(() => {
showMessage(false);
setVideo(true);
}, 4000);
}, 2000);
};
const hideVideo = () => {
console.log(video);
showMessage(false);
if (video) {
setVideo(false);
}
};
const test = (event) => {
event.stopPropagation();
console.log(video)
}
return (
<>
{message && <h1>Message</h1>}
{video && <h2>Video</h2>}
<button onClick={test}>test</button>
</>
);
}

Related

react-countdown is not reseting or re-rendering second time

What I am trying to do is to update the reset the countdown after changing the status.
There are three status that i am fetching from API .. future, live and expired
If API is returning future with a timestamp, this timestamp is the start_time of the auction, but if the status is live then the timestamp is the end_time of the auction.
So in the following code I am calling api in useEffect to fetch initial data pass to the Countdown and it works, but on 1st complete in handleRenderer i am checking its status and updating the auctionStatus while useEffect is checking the updates to recall API for new timestamp .. so far its working and 2nd timestamp showed up but it is stopped ... means not counting down time for 2nd time.
import React, { useEffect } from 'react';
import { atom, useAtom } from 'jotai';
import { startTimeAtom, auctionStatusAtom } from '../../atoms';
import { toLocalDateTime } from '../../utility';
import Countdown from 'react-countdown';
import { getCurrentAuctionStatus } from '../../services/api';
async function getAuctionStatus() {
let response = await getCurrentAuctionStatus(WpaReactUi.auction_id);
return await response.payload();
}
const Counter = () => {
// component states
const [startTime, setStartTime] = useAtom(startTimeAtom);
const [auctionStatus, setAuctionStatus] = useAtom(auctionStatusAtom);
useEffect(() => {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
setStartTime(toLocalDateTime(response.end_time, WpaReactUi.time_zone));
});
}, [auctionStatus]);
//
const handleRenderer = ({ completed, formatted }) => {
if (completed) {
console.log("auction status now is:", auctionStatus);
setTimeout(() => {
if (auctionStatus === 'future') {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
});
}
}, 2000)
}
return Object.keys(formatted).map((key) => {
return (
<div key={`${key}`} className={`countDown bordered ${key}-box`}>
<span className={`num item ${key}`}>{formatted[key]}</span>
<span>{key}</span>
</div>
);
});
};
console.log('starttime now:', startTime);
return (
startTime && (
<div className="bidAuctionCounterContainer">
<div className="bidAuctionCounterInner">
<Countdown
key={auctionStatus}
autoStart={true}
id="bidAuctioncounter"
date={startTime}
intervalDelay={0}
precision={3}
renderer={handleRenderer}
/>
</div>
</div>
)
);
};
export default Counter;
You use auctionStatus as a dependency for useEffect.
And when response.status is the same, the auctionStatus doesn't change, so your useEffect won't be called again.
For answering your comment on how to resolve the issue..
I am not sure of your logic but I'll explain by this simple example.
export function App() {
// set state to 'live' by default
const [auctionStatus, setAuctionStatus] = React.useState("live")
React.useEffect(() => {
console.log('hello')
changeState()
}, [auctionStatus])
function changeState() {
// This line won't result in calling your useEffect
// setAuctionStatus("live") // 'hello' will be printed one time only.
// You need to use a state value that won't be similar to the previous one.
setAuctionStatus("inactive") // useEffect will be called and 'hello' will be printed twice.
}
}
You can simply use a flag instead that will keep on changing from true to false like this:
const [flag, setFlag] = React.useState(true)
useEffect(() => {
// ..
}, [flag])
// And in handleRenderer
getAuctionStatus().then((response) => {
setFlag(!flag);
});
Have a look at the following useCountdown hook:
https://codepen.io/AdamMorsi/pen/eYMpxOQ
const DEFAULT_TIME_IN_SECONDS = 60;
const useCountdown = ({ initialCounter, callback }) => {
const _initialCounter = initialCounter ?? DEFAULT_TIME_IN_SECONDS,
[resume, setResume] = useState(0),
[counter, setCounter] = useState(_initialCounter),
initial = useRef(_initialCounter),
intervalRef = useRef(null),
[isPause, setIsPause] = useState(false),
isStopBtnDisabled = counter === 0,
isPauseBtnDisabled = isPause || counter === 0,
isResumeBtnDisabled = !isPause;
const stopCounter = useCallback(() => {
clearInterval(intervalRef.current);
setCounter(0);
setIsPause(false);
}, []);
const startCounter = useCallback(
(seconds = initial.current) => {
intervalRef.current = setInterval(() => {
const newCounter = seconds--;
if (newCounter >= 0) {
setCounter(newCounter);
callback && callback(newCounter);
} else {
stopCounter();
}
}, 1000);
},
[stopCounter]
);
const pauseCounter = () => {
setResume(counter);
setIsPause(true);
clearInterval(intervalRef.current);
};
const resumeCounter = () => {
setResume(0);
setIsPause(false);
};
const resetCounter = useCallback(() => {
if (intervalRef.current) {
stopCounter();
}
setCounter(initial.current);
startCounter(initial.current - 1);
}, [startCounter, stopCounter]);
useEffect(() => {
resetCounter();
}, [resetCounter]);
useEffect(() => {
return () => {
stopCounter();
};
}, [stopCounter]);
return [
counter,
resetCounter,
stopCounter,
pauseCounter,
resumeCounter,
isStopBtnDisabled,
isPauseBtnDisabled,
isResumeBtnDisabled,
];
};

Custom Confirm Modal in React using Context

How can I implement native javascript style confirm modal using context api?
I made a codesandbox.
https://codesandbox.io/s/little-sunset-806rdh?file=/src/App.js
Or please see below code.
context.js
const confirmContext = createContext()
function ConfirmProvider({children}){
const [visible, setVisible] = useState(false)
const [message, setMessage] = useState('')
const open = (message) => {
setVisible(true)
setMessage(message)
}
const handleSubmitClick = () => { }
const handleCancelClick = () => { }
if (!visible) return <div />
return (
<ConfirmContext.Provider value={{ open }}>
<div>
{message}
<button onClick={handleSubmitClick}>OK</button>
<button onClick={handleCancelClick}>Cancel</button>
</div>
{children}
</ConfirmContext.Provider>
)
}
const useConfirm = () => useContext(ConfirmContext)
export { ConfirmProvider }
delete.js
function Delete(){
const confirm = useConfirm()
const handleClick = () => {
confirm.open('Are you really delete it?')
// How do I code here when 'OK' or 'Cancel' Button Click?
// if(confirm.open()){
// // do something
// } else {
// // do nothing
// }
}
return (
<div>
<button onClick={handleClick}>Delete</button>
</div>
)
}
return Promise in open function will implement this pattern.
const open = ({ message, confirmText, cancelText }) => {
setVisible(true)
setMessage(message)
return new Promise((res, rej) => {
resolveCallback = res
})
}
const onConfirmClick = () => {
window.alert('on confirm click')
resolveCallback(true)
setVisible(false)
}
const onCancelClick = () => {
window.alert('on cancel click')
resolveCallback(false)
setVisible(false)
}
// use
const c = confirm.open('you really delete it?')
if(c) // do something

React click specific element in setInterval loop

I'm trying to click my element in setInterval loop, so it would be clicked every 10 second, but there's always error click is not a function or cannot read click null
I've tired with useRef and also did nothing.
here is my code:
useEffect(() => {
setInterval(function () {
const handleChangeState = () => {
console.log("Now");
document.getElementById("dice").click();
};
handleChangeState();
}, 10 * 1000);
}, []);
return (
<>
<Dice id="dice" rollingTime="3000" triggers={["click", "P"]} />
</>
);
};
It is often considered anti-pattern in React to query the DOM. You should instead use a React ref to gain access to the underlying DOMNode.
There are a couple ways to use a React ref to invoke a dice roll of the child component. FYI, rollingTime should probably be number type instead of a string if using in any setTimeout calls.
Forward the React ref and attach to the button element and invoke the click handler.
Example:
const Dice = forwardRef(({ id, rollingTime }, ref) => {
const timerRef = useRef();
const [value, setValue] = useState();
const [isRolling, setIsRolling] = useState();
useEffect(() => {
return () => clearTimeout(timerRef.current);
}, []);
const roll = () => {
if (!isRolling) {
setIsRolling(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setValue(Math.floor(Math.random() * 6) + 1);
setIsRolling(false);
}, rollingTime);
}
};
return (
<>
<h1>Dice</h1>
<h2>Roll Value: {isRolling ? "Rolling..." : value}</h2>
<button ref={ref} id={id} type="button" onClick={roll}>
Roll the dice
</button>
</>
);
});
...
export default function App() {
const diceRef = useRef();
useEffect(() => {
const handleChangeState = () => {
console.log("Clicking Dice");
diceRef.current?.click();
};
setInterval(() => {
handleChangeState();
}, 10 * 1000);
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}
Forward the React ref and invoke the button's callback function directly via the useImperativeHandle hook.
Example:
const Dice = forwardRef(({ id, rollingTime }, ref) => {
const timerRef = useRef();
const [value, setValue] = useState();
const [isRolling, setIsRolling] = useState();
useEffect(() => {
return () => clearTimeout(timerRef.current);
}, []);
const roll = () => {
if (!isRolling) {
setIsRolling(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setValue(Math.floor(Math.random() * 6) + 1);
setIsRolling(false);
}, rollingTime);
}
};
useImperativeHandle(ref, () => ({
roll
}));
return (
<>
<h1>Dice 2</h1>
<h2>Roll Value: {isRolling ? "Rolling..." : value}</h2>
<button id={id} type="button" onClick={roll}>
Roll the dice
</button>
</>
);
});
...
export default function App() {
const diceRef = useRef();
useEffect(() => {
const handleRollDice = () => {
console.log("Roll dice");
diceRef.current.roll();
};
setInterval(() => {
handleRollDice();
}, 10 * 1000);
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}
Using react-dice-roll
If you examine the react-dice-roll source code you'll see that the Dice component forwards a React ref and uses the useImperativeHandle hook to expose out a rollDice function.
Dice Source
const Dice = forwardRef((props: TProps, ref: React.MutableRefObject<TDiceRef>) => {
...
const handleDiceRoll = (value?: TValue) => {
let diceAudio: HTMLAudioElement;
if (sound) {
diceAudio = new Audio(sound);
diceAudio.play();
}
setRolling(true);
setTimeout(() => {
let rollValue = Math.floor((Math.random() * 6) + 1) as TValue;
if (value) rollValue = value;
if (cheatValue) rollValue = cheatValue;
setRolling(false);
setValue(rollValue);
if (diceAudio) diceAudio.pause();
if (!onRoll) return;
onRoll(rollValue);
}, rollingTime);
};
useImperativeHandle(ref, () => ({ rollDice: handleDiceRoll }));
...
return (
...
)
});
Your code then just needs to create a React ref and pass it to the Dice component, and instantiate the interval in a mounting useEffect hook.
Example:
function App() {
const diceRef = useRef();
useEffect(() => {
const rollDice = () => {
console.log("Rolling Dice");
diceRef.current.rollDice(); // <-- call rollDice function
};
// instantiate interval
setInterval(() => {
rollDice();
}, 10 * 1000);
// immediately invoke so we don't wait 10 seconds for first roll
rollDice();
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}

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>
);
};

Props in react seems to not be usable right away

I have a small issue with a really simple component that doesn't display what I want.
const UserCards = (props) => {
const [retrievedData, setRetrievedData] = useState();
useEffect(() => {
const data = [];
props.users.map((user) => {
data.push(<UserCard key={user.username} user={user} />);
});
setRetrievedData(data);
}, []);
return (
<div className={styles.userCards}>{retrievedData && retrievedData}</div>
);
};
When I refresh the page it will not display my User cards. But If I had a timeout on useEffect like this :
const UserCards = (props) => {
const [retrievedData, setRetrievedData] = useState();
useEffect(() => {
const data = [];
setTimeout(function () {
props.users.map((user) => {
data.push(<UserCard key={user.username} user={user} />);
});
setRetrievedData(data);
}, 3000);
}, []);
return (
<div className={styles.userCards}>{retrievedData && retrievedData}</div>
);
};
Everything's fine!
I thought props were usable immediately but it seems I was wrong.
I tried to add [props] at the end of useEffect to be sure my state will be updated if props changed, but nothing...
I'm sure it's nothing but I've been struggling since yesterday!
Thank you!
Just add useEffect dependency, which will call your useEffect content every time, when dependency changed:
const UserCards = (props) => {
const [retrievedData, setRetrievedData] = useState();
useEffect(() => {
const data = [];
props.users.map((user) => {
data.push(<UserCard key={user.username} user={user} />);
});
setRetrievedData(data);
}, [props]);
return (
<div className={styles.userCards}>{retrievedData && retrievedData}</div>
);
};

Resources