When accessing a state variable from a useCallback, value is not updated - reactjs

At a certain place of my code I'm accessing a state variable of my component from a call back ( UserCallback ) and I find the state variable has not updated from the initial value and call back is referring to the initial value. As I read in the documentation when variable is passed as one of array items then it should update the function when it is updated. Following is a sample code.
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const node = useRef(null);
useImperativeHandle(ref, () => ({
increment() {
setCount(count + 1);
}
}));
const clickListener = useCallback(
e => {
if (!node.current.contains(e.target)) {
alert(count);
}
},
[count]
);
useEffect(() => {
// Attach the listeners on component mount.
document.addEventListener("click", clickListener);
// Detach the listeners on component unmount.
return () => {
document.removeEventListener("click", clickListener);
};
}, []);
return (
<div
ref={node}
style={{ width: "500px", height: "100px", backgroundColor: "yellow" }}
>
<h1>Hi {count}</h1>
</div>
);
});
const Parent = () => {
const childRef = useRef();
return (
<div>
<Child ref={childRef} />
<button onClick={() => childRef.current.increment()}>Click</button>
</div>
);
};
export default function App() {
return (
<div className="App">
<Parent />
</div>
);
}
What I'm originally building is a custom confirmation modal. I have a state variable which set either display:block or display:none to the root element. Then if there is a click outside the component I need to close the modal by setting state variable to false. Following is the original function.
const clickListener = useCallback(
(e: MouseEvent) => {
console.log('isVisible - ', isVisible, ' count - ', count, ' !node.current.contains(e.target) - ', !node.current.contains(e.target))
if (isVisible && !node.current.contains(e.target)) {
setIsVisible(false)
}
},
[node.current, isVisible],
)
It doesn't get closed because isVisible is always false which is the initial value.
What am I doing wrong here?
For further clarifications following is the full component.
const ConfirmActionModal = (props, ref) => {
const [isVisible, setIsVisible] = useState(false)
const [count, setCount] = useState(0)
const showModal = () => {
setIsVisible(true)
setCount(1)
}
useImperativeHandle(ref, () => {
return {
showModal: showModal
}
});
const node = useRef(null)
const stateRef = useRef(isVisible);
const escapeListener = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') {
setIsVisible(false)
}
}, [])
useEffect(() => {
stateRef.current = isVisible;
}, [isVisible]);
useEffect(() => {
const clickListener = e => {
if (stateRef.current && !node.current.contains(e.target)) {
setIsVisible(false)
}
};
// Attach the listeners on component mount.
document.addEventListener('click', clickListener)
document.addEventListener('keyup', escapeListener)
// Detach the listeners on component unmount.
return () => {
document.removeEventListener('click', clickListener)
document.removeEventListener('keyup', escapeListener)
}
}, [])
return (
<div ref={node}>
<ConfirmPanel style={{ display : isVisible ? 'block': 'none'}}>
<ConfirmMessage>
Complete - {isVisible.toString()} - {count}
</ConfirmMessage>
<PrimaryButton
type="submit"
style={{
backgroundColor: "#00aa10",
color: "white",
marginRight: "10px",
margin: "auto"
}}
onClick={() => {console.log(isVisible); setCount(2)}}
>Confirm</PrimaryButton>
</ConfirmPanel>
</div>
)
}
export default forwardRef(ConfirmActionModal)

You assign a function clickListener to document.addEventListener on component mount, this function has a closure on count value.
On the next render, the count value will be stale.
One way to solve it is implementing a function with refernce closure instead:
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
// countRef.current always holds the most updated state
const clickListener = e => {
if (!node.current.contains(e.target)) {
alert(countRef.current);
}
};
document.addEventListener("click", clickListener);
return () => {
document.removeEventListener("click", clickListener);
};
}, []);
...
}

You can pass a callback to setIsvisible so you don't need isVisible as a dependency of the useCallback. Adding node.current is pointless since node is a ref and gets mutated:
const clickListener = useCallback((e) => {
setIsVisible((isVisible) => {//pass callback to state setter
if (isVisible && !node.current.contains(e.target)) {
return false;
}
return isVisible;
});
}, []);//no dependencies needed

While your clickListener does change when count changes you only bind the initial clickListener once on mount because your useEffect dependency list is empty. You could ad clickListener to the dependency list as well:
useEffect(() => {
// Attach the listeners on component mount.
document.addEventListener("click", clickListener);
// Detach the listeners on component unmount.
return () => {
document.removeEventListener("click", clickListener);
};
}, [clickListener]);
Side note: using node.current in a dependency list doesn't do anything as react does not notice any changes to a ref. Dependencies can only be state or props.

Related

why useRef current value , isn't sharing trough custom hook?

I wanted to calculate the user scroll height , so I created a custom hook. and I wanted to share this value to another component. but it doesnt work.
code:
const useScroll = () => {
let scrollHeight = useRef(0);
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", () => {});
};
}, []);
return scrollHeight.current;
};
export default useScroll;
the value is not updating here.
but if I use useState here , it works. but that causes tremendous amount of component re-rendering. can you have any idea , how its happening?
Since the hook won't rerender you will only get the return value once. What you can do, is to create a useRef-const in the useScroll hook. The useScroll hook returns the reference of the useRef-const when the hook gets mounted. Because it's a reference you can write the changes in the useScroll hook to the useRef-const and read it's newest value in a component which implemented the hook. To reduce multiple event listeners you should implement the hook once in the parent component and pass the useRef-const reference to the child components. I made an example for you.
The hook:
import { useCallback, useEffect, useRef } from "react";
export const useScroll = () => {
const userScrollHeight = useRef(0);
const scroll = useCallback(() => {
userScrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
}, []);
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", scroll);
};
}, []);
return userScrollHeight;
};
The parent component:
import { SomeChild, SomeOtherChild } from "./SomeChildren";
import { useScroll } from "./ScrollHook";
const App = () => {
const userScrollHeight = useScroll();
return (
<div>
<SomeChild userScrollHeight={userScrollHeight} />
<SomeOtherChild userScrollHeight={userScrollHeight} />
</div>
);
};
export default App;
The child components:
export const SomeChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "aqua"
}}>
<h1>SomeChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
export const SomeOtherChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeOtherChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "orange"
}}>
<h1>SomeOtherChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
import { useRef } from 'react';
import throttle from 'lodash.throttle';
/**
* Hook to return the throttled function
* #param fn function to throttl
* #param delay throttl delay
*/
const useThrottle = (fn, delay = 500) => {
// https://stackoverflow.com/a/64856090/11667949
const throttledFn = useRef(throttle(fn, delay)).current;
return throttledFn;
};
export default useThrottle;
then, in your custom hook:
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
const throttledScroll = useThrottle(scroll)
Also, I like to point out that you are not clearing your effect. You should be:
useEffect(() => {
window.addEventListener("scroll", throttledScroll);
return () => {
window.removeEventListener("scroll", throttledScroll); // remove Listener
};
}, [throttledScroll]); // this will never change, but it is good to add it here. (We've also cleaned up effect)

How to Use componentDidMount() in Functional Component to execute a function

I have a functional component which had a button to call a method in it. Now i want to get rid of the button and call that method without any actions once the component loads.
I am making API calls inside this method and passing on the results to another component.
Also I am replacing the button with a progress bar meaning when a "search" is taking place, display the progress bar but I am having no luck. What am I doing wrong ?
export const Search = (props) => {
const { contacts, setContacts, onSearchComplete } = props;
const [msgBox, setMsgBox] = useState(null);
const [loading, setLoading] = useState(false);
const onSearch = async () => {
setLoading(true);
const emails = contacts
.filter(x => x.isChecked)
.map(item => item.emailAddress);
try {
const searchResults = await AppApi.searchMany(emails);
let userList = [];
for (let i = 0; i < searchResults.length; i++) {
//process the list and filter
}
userList = [...userList, ..._users];
}
onSearchComplete(userList); //passing the results.
} catch (err) {
console.log({ err });
setMsgBox({ message: `${err.message}`, type: 'error' });
}
setLoading(false);
}
return (
<Box>
{loading ? <LinearProgress /> : <Box>{msgBox && (<a style={{ cursor: 'pointer' }} onClick={() => setMsgBox(null)} title="Click to dismiss"><MessageBox type={msgBox.type || 'info'}>{msgBox.message}</MessageBox></a>)}</Box>}
/*{onSearch()}*/ // function that was executed onclick.
</Box>
);
}
You will want to use the useEffect hook with an empty dependency array which will make it act as componentDidMount source.
export const Search = (props) => {
const { contacts, setContacts, onSearchComplete } = props;
const [msgBox, setMsgBox] = useState(null);
const [loading, setLoading] = useState(false);
const onSearch = async () => {
...
}
useEffect(() => {
onSearch();
}, []);
return (
<Box>
{loading ? <LinearProgress /> : <Box>{msgBox && (<a style={{ cursor: 'pointer' }} onClick={() => setMsgBox(null)} title="Click to dismiss"><MessageBox type={msgBox.type || 'info'}>{msgBox.message}</MessageBox></a>)}</Box>}
</Box>
);
}

How do you prevent the component from disappearing too soon?

const FlashMessage = (props) => {
const [isOpen, setIsOpen] = useState(true);
const hideComponent = () => {
setisOpen(false);
};
useEffect(() => {
setIsOpen(true);
setTimeout(() => hideComponent(), 9000);
}, [props]);
return (
(props.flashMessage === true && isOpen) ?
<View style={styles.main}>
<Text style={styles.message}>{props.message}</Text>
</View>
: null
);
}
I have this Flash Message component in my React Native app, and sometimes, the Flash Message disappears after 2 second. It seems to appear on a random basis and it's probably due to a problem with useEffect and setTimeout, but I have not been able to figure out what might be causing this.
The effect you have with [props] as dependency doesn't make sense to me.
But you can have an isolated effect for the isOpen boolean.
useEffect(() => {
setTimeout(() => {
setIsOpen(false);
}, 9000);
}, [isOpen]);
Here is a full working example, simplified:
export default function App() {
const [show, setShow] = useState(false);
useEffect(() => {
setTimeout(() => {
setShow(false);
}, 2000);
}, [show]);
return (
<div className="App">
<button onClick={e => setShow(true)}>Show</button>
{show && <div>hello</div>}
</div>
);
}

Strange React hooks behavior, can't access new state from a function

I use the library react-use-modal, and
I'm trying to read the updated value of confirmLoading when inside the handleClick function.
handleClick does read the first value of confirmLoading defined when doing const [ confirmLoading, setConfirmLoading ] = useState(false), but never updates when I setConfirmLoading inside handleOk.
I don't understand what I'm doing wrong
import { Button, Modal as ModalAntd } from 'antd'
import { useModal } from 'react-use-modal'
export interface ModalFormProps {
form: React.ReactElement
}
export const ModalForm: React.FC = () => {
const [ confirmLoading, setConfirmLoading ] = useState(false)
const { showModal, closeModal } = useModal()
const handleOk = () => {
setConfirmLoading(true)
setTimeout(() => {
setConfirmLoading(false)
closeModal()
}, 1000)
}
const handleCancel = () => {
closeModal()
}
const handleClick = () => {
showModal(({ show }) => (
<ModalAntd
onCancel={handleCancel}
onOk={handleOk}
title='Title'
visible={show}
>
// the value of confirmLoading is always the one defined
// with useState at the beginning of the file.
<p>{confirmLoading ? 'yes' : 'no'}</p>
</ModalAntd>
))
}
return (
<div>
<Button onClick={handleClick}>
Open Modal
</Button>
</div>
)
}
This is happening because of closures. The component that you pass to showModal remembers confirmLoading and when you call function setConfirmLoading your component renders again and function handleClick is recreated. 'Old' handleClick and 'old' component in showModal know nothing about the new value in confirmLoading.
Try to do this:
export const ModalForm: React.FC = () => {
const { showModal, closeModal } = useModal();
const handleClick = () => {
showModal(({ show }) => {
const [ confirmLoading, setConfirmLoading ] = useState(false);
const handleOk = () => {
setConfirmLoading(true)
setTimeout(() => {
setConfirmLoading(false)
closeModal()
}, 1000)
};
const handleCancel = () => {
closeModal()
};
return (
<ModalAntd
onCancel={handleCancel}
onOk={handleOk}
title='Title'
visible={show}
>
// the value of confirmLoading is always the one defined
// with useState at the beginning of the file.
<p>{confirmLoading ? 'yes' : 'no'}</p>
</ModalAntd>
);
})
};
return (
<div>
<Button onClick={handleClick}>
Open Modal
</Button>
</div>
)
}

Ref issue and Calling custom hook after useEffect

I am using a custom hook to detect outside clicks
const useClickOutside = (nodeElement, handler) => {
function handleClickOutside(event) {
if (nodeElement && !nodeElement.contains(event.target)) {
handler();
}
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
}
And I am calling it like this
const Modal = ({ ... }) => {
const modalRef = useRef(null);
console.log(modalRef.current) // null
useEffect(() => {
console.log(modalRef.current) // work fine here and display the dom element
}, [])
// here the hooks it is called with modalRef.current as null
useClickOutside(modalRef.current, () => {
dispatch(hideModal());
});
return (
<div className="pop-container">
<div className="pop-dialog" ref={modalRef}>
...
</div>
</div>
)
}
The problem is that my custom hooks useClickOutside is called with modalRef.current as null
And as you see in the useEffet hook the modalRef.current value is correct
However i can't call my custom hook there in useEffet otherwise i will get Uncaught Invariant Violation: Hooks can only be called inside the body of a function component
So how to solve this issue ?
Instead of passing ref.current, if you just pass ref, your code will work since ref.current will be mutated at its reference when ref is assigned
const useClickOutside = (nodeElement, handler) => {
function handleClickOutside(event) {
if (nodeElement.current && !nodeElement.current.contains(event.target)) {
handler();
}
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
}
and in modal
const Modal = ({ ... }) => {
const modalRef = useRef(null);
// here the hooks it is called with modalRef.current as null
useClickOutside(modalRef, () => {
dispatch(hideModal());
});
return (
<div className="pop-container">
<div className="pop-dialog" ref={modalRef}>
...
</div>
</div>
)
}
Working demo

Resources