React state not consistent when using delay - javascript

I have a component with another component inside it that I only want to show if the user is still hovering after a short delay.
So I wrote a couple handlers for onMouseEnter and onMouseExit to set a variable to show that its hovered. Then, after sleeping, if it's still hovered I want to set a second variable to show the element.
However, hovered is showing as false no matter what. Why?
const [hovered, setHovered] = useState(false);
const [show, setShow] = useState(false);
console.log('hovered', hovered); // This shows the state correctly
const handleEnter = () => {
setHovered(true);
sleep(2000).then(() => {
console.log('checking', hovered); // This always shows false
if (hovered) {
setShow(true);
}
});
}
const handleExit = () => {
setHovered(false);
setShow(false);
}
Edit
Solution:
Replace sleep with a wrapped callback from use-debounce to prevent it from firing multiple times, and have the delay work still.
const ref = useRef(false);
const [hovered, setHovered] = useState(false);
const [show, setShow] = useState(false);
const handleHover = useDebouncedCallback(() => {
if (ref.current) setShow(true);
else setShow(false);
}, 1000);
useEffect(() => {
ref.current = hovered;
}, [hovered]);
useEffect(() => {
handleHover()
}, [hovered]);

i would recommend you to use useRef hook.
const hoveredRef = useRef(false);
const [hovered, setHovered] = useState(false);
const [show, setShow] = useState(false);
useEffect(() => {
hoveredRef.current = hovered;
}, [hovered])
useEffect(() => {
if (!hovered) return;
sleep(2000).then(() => {
console.log('checking', hoveredRef.current);
if (hoveredRef.current) {
setShow(true);
}
}, [hovered])
const handleEnter = () => {
setHovered(true);
}
const handleExit = () => {
setHovered(false);
setShow(false);
}
I didnt check it but should be ok, i dont have the rest of your code, sorry
Regarding your question Why:
const handleEnter = () => {
setHovered(true);
sleep(2000).then(() => {
console.log('checking', hovered); // This always shows false
if (hovered) {
setShow(true);
}
});
}
SetHover that you are calling here does not change the variable immediately, for all the code after this line in function it will still be false. And in the sleep.then function scope it still uses a closure-captured old "false" value.
And a little warning here, code above is not ideal, if you hover in-out 5 times within your 2 second delay - code inside sleep.then will fire 5 times successfully. But it is harmless in your case, in terms of behavior.

Related

Way to invoke function again while not setting different value in state

So I have built app which takes value from input -> set it to the state-> state change triggers functions in useEffect (this part is in custom hook) -> functions fetch data from api -> which triggers functions in useEffect in component to store data in array. The thing is that there are two problems that I am trying to solve :
When user is putting the same value in input and setting it in state it's not triggering useEffect functions (I solved it by wrapping value in object but I am looking for better solution).
When user uses the same value in short period of time api will send the same data which again makes problem with triggering function with useEffect (I tried to solved with refresh state that you will see in code below, but it looks awful)
The question is how can I actually do it properly? Or maybe the solutions I found aren't as bad as I think they are. Thanks for your help.
component
const [nextLink, setNextLink] = useState({ value: "" });
const isMounted = useRef(false);
const inputRef = useRef(null);
const { shortLink, loading, error, refresh } = useFetchLink(nextLink);
const handleClick = () => {
setNextLink({ value: inputRef.current.value });
};
useEffect(() => {
setLinkArr((prev) => [
...prev,
{
id: prev.length === 0 ? 1 : prev[prev.length - 1].id + 1,
long: nextLink.value,
short: shortLink,
},
]);
if (isMounted.current) {
scrollToLink();
} else {
isMounted.current = true;
}
inputRef.current.value = "";
}, [refresh]);
custom hook
const useFetchLink = (linkToShorten) => {
const [shortLink, setShortLink] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [refresh, setRefresh] = useState(false);
const isMounted = useRef(false);
const fetchLink = async (link) => {
setLoading(true);
try {
const response = await fetch(
`https://api.shrtco.de/v2/shorten?url=${link}`
);
if (response.ok) {
const data = await response.json();
setShortLink(data.result.short_link);
setRefresh((prev) => !prev);
} else {
throw response.status;
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isMounted.current) {
if (checkLink(linkToShorten.value)) {
setError(checkLink(linkToShorten.value));
} else {
fetchLink(linkToShorten.value);
}
} else {
isMounted.current = true;
}
}, [linkToShorten]);
const value = { shortLink, loading, error, refresh };
return value;
};
export default useFetchLink;

Can I change the state for given time and then revert it back to the original state using settimeout

I want to get the value to true for one second, after which it will revert back to false using settimeout but stuck. Here is my code:
const [value, setValue] = useState(false)
const handleClick = () => {
setTimeout(() => {
setValue(!value)
console.log(value);
},1000);
console.log(value);
}
const [value, setValue] = useState(false);
const timeoutId = useRef(null);
const handleClick = () => {
// clear existing timeout
clearTimeout(timeoutId.current);
// set the value to true
setValue(true);
// revert the value to false after 1s
timeoutId.current = setTimeout(() => setValue(false), 1000);
}

React making signup checkbox

We are making the necessary consent for membership registration with React. However, it is up to agreeing to the checkbox, but it does not work properly because it is re-rendered when the button is clicked. Any help would be appreciated.
enter code here
const [disabled, setDisabled] = useState('disabled');
const [agree1, setAgree1] = useState(false); //회원정보 동의
const [agree2, setAgree2] = useState(false); //개인정보 수집 및 이용동의
const [agree3, setAgree3] = useState(false); //위치정보 동의
const [total, settotal] = useState(false); //전체 동의
const [email, setemail] = useState("");
const buttonState = useCallback(() => {
if((agree1===true)&&(agree2===true)&&(agree3===true)){
settotal(true);
setDisabled('');
}
else {
setDisabled('disabled');
}
},[agree1,agree2,agree3,total]);
useEffect(() => {
buttonState();
}, [buttonState])
const totalchange = () => {
if(total ===true) { //전체동의가 true라면 다시 클릭 했을때 전부 unchecked
settotal(!total);
setAgree1(!agree1);
setAgree2(!agree2);
setAgree3(!agree3);
} else{ //그외(하나만 체크되 있거나 아무것도 없다면) 전부 checked로 만듬
settotal(true);
setAgree1(true);
setAgree2(true);
setAgree3(true);
setDisabled('');
}
}
const clickFunction = () => {
axios.get(`/signup/authNum?email=${email}`)
.then(res => {
if(window.confirm("인증번호 전송이 완료되었습니다")){
history.push({
pathname : '/registerauth',
state : {state : res.data}
})
}
})
.catch(err => {console.log(err)})
}enter code here
Sorry for being awkward because this is my first question on stack-overflow
Why are you using the callback for the button click and why are you calling that function in useEffect?

How to modify the code such that can call usehook using react and typescript?

A popup is displayed when the add button is clicked and the count is greater or less than 0.
below is the code,
function AddButton () {
const [isOpen, setIsOpen] = React.useState(false);
const count = useGetCount();
useTrigger(isOpen, count);
const on_add_click = () => {
setIsOpen(true);
}
return (
<button onClick={on_add_click}>add</button>
);
}
interface ContextProps {
trigger: (count: number) => void;
}
const popupContext = React.createContext<ContextProps>({
trigger: (count: number) => {},
});
const usePopupContext = () => React.useContext(popupContext);
export const popupContextProvider = ({ children }: any) => {
const [show, setShow] = React.useState(false);
const limit = 0;
const dismiss = () => {
if (show) {
sessionStorage.setItem(somePopupId, 'dismissed');
setShow(false);
}
};
const isDismissed = (dialogId: string) =>
sessionStorage.getItem(dialogId) === 'dismissed';
const context = {
trigger: (count: number) => {
if (!isDismissed(somePopupId) && count <= limit) {
setShow(true);
} else if (count > limit) {
setShow(false);
}
},
};
return (
<popupContext.Provider value={context}>
{children}
{show && (
<Popup onHide={dismiss} />
)}
</popupContext.Provider>
);
};
export function useTrigger(enabled: boolean, count: number) {
const { trigger } = usePopupContext();
React.useEffect(() => {
if (enabled) {
trigger(count);
}
}, [enabled, count, trigger]);
}
This works but calls trigger method only when enabled is true.
I want to modify the above code such that when the user clicks the add button, I want this useTrigger to happen. I don't want to check for enabled and call trigger.
I have tried the following removed checking for enabled.
export function useTrigger(enabled: boolean, count: number) {
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}, [enabled, count, trigger]);
}
this works but the popup is displayed as the count is less than or equal to 0. but I want it to first check if the add button clicked or not.
so soon after the user clicking the add button in on_add_click I want the popup to display.
How can I modify the code above? I am new to using hooks. Could someone help me with this? thanks.
EDIT:strong text
i have tried to do something like below and i get error
Uncaught Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component.
on_add_click = () => {
Trigger(count);
}
export function Trigger(count: number) {
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}, [count, trigger]);
}
how can i fix this.
If I followed you right, you just need to add a state to your hook and return the setter to call it onclick:
export function useTrigger(count: number) {
const [clicked, setClicked] = React.useState(false)
const { trigger } = usePopupContext();
React.useEffect(() => {
if(clicked) {
trigger(count);
}
}, [count, trigger, clicked]);
const clickCb = useCallback(() => {setClicked(true)}, [])
return cb
}
Then in your component with button you do something like this:
const Component = (props) => {
const onClick = useTrigger(props.count)
/* ... */
return <button onClick={onClick}/>
}

React Hook : Correct way of using custom hook to handle onClick Event?

As the title said, what is the correct way of using custom hook to handle onClick Event?
This codesandbox application will display a new quote on the screen when user clicks the search button.
function App() {
const [{ data, isLoading, isError }, doFetch] = useDataApi(
"https://api.quotable.io/random"
);
return (
<Fragment>
<button disabled={isLoading} onClick={doFetch}>
Search
</button>
{isError && <div>Something went wrong ...</div>}
{isLoading ? <div>Loading ...</div> : <div>{data.content}</div>}
</Fragment>
);
}
I created a custom hook called useDataApi() which would fetch a new quote from an API. In order to update the quote when the user clicks the button, inside the useDataApi(), I created a handleClick() which will change the value of a click value to trigger re-render. And this handleClick() function will be return back to App()
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [click, setClick] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const handleClick = () => {
setClick(!click);
};
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [initialUrl, click]);
return [{ data, isLoading, isError }, handleClick];
};
This is working, however, I don't feel this is the correct solution.
I also tried moving the fetchData() out of useEffect and return the fetchData(), and it works too. But according to the React Doc, it says it is recommended to move functions inside the useEffect.
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
useEffect(() => {
fetchData();
}, []);
return [{ data, isLoading, isError }, fetchData];
};
In addition, for creating these kinds of application, is the way that I am using is fine or there is another correct solution such as not using any useEffects or not create any custom Hook?
Thanks
Not sure if this is correct, but here is my solution.
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const doFetch = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
return [{ data, isLoading, isError }, doFetch];
};
Btw, don't mutate state directly.
const handleClick = () => {
setClick(!click); // don't do this
setClick(prev => !prev); // use this
};
Your implementation is fine. We are also using something similar. Hope you find it useful.
function useApi(promiseFunction, deps, shouldRun=true){
// promisFunction returns promise
const [loading, setLoading] = useState(false)
const [data, setData] = useState(false)
const [error, setError] = useState(false)
const dependencies: any[] = useMemo(()=>{
return [...dependencyArray, shouldRun]
},[...dependencyArray, shouldRun])
const reload = () => {
async function call() {
try {
setError(null)
setLoading(true)
const res = await promiseFunction();
}
catch (error) {
setError(error)
}
finally {
setLoading(false)
}
}
call();
}
useEffect(() => {
if(!shouldRun) return
setResult(null) //no stale data
reload()
}, dependencies)
return {loading, error, data, reload, setState: setData}
}
Below code will provide some idea about how to use it.
function getUsersList(){
return fetch('/users')
}
function getUserDetail(id){
return fetch(`/user/${id}`)
}
const {loading, error, data } = useApi(getUsersList, [], true)
const {loading: userLoading, error: userError, data: userData}
= useApi(()=>getUserDetail(id), [id], true)

Categories