Hello I'm trying to pass the following code to reacthooks:
import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';
class SomeComponent extends React.Component {
// 2. Initialise your ref and targetElement here
targetRef = React.createRef();
targetElement = null;
componentDidMount() {
// 3. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav).
// Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element).
// This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired.
this.targetElement = this.targetRef.current;
}
showTargetElement = () => {
// ... some logic to show target element
// 4. Disable body scroll
disableBodyScroll(this.targetElement);
};
hideTargetElement = () => {
// ... some logic to hide target element
// 5. Re-enable body scroll
enableBodyScroll(this.targetElement);
}
componentWillUnmount() {
// 5. Useful if we have called disableBodyScroll for multiple target elements,
// and we just want a kill-switch to undo all that.
// OR useful for if the `hideTargetElement()` function got circumvented eg. visitor
// clicks a link which takes him/her to a different page within the app.
clearAllBodyScrollLocks();
}
render() {
return (
// 6. Pass your ref with the reference to the targetElement to SomeOtherComponent
<SomeOtherComponent ref={this.targetRef}>
some JSX to go here
</SomeOtherComponent>
);
}
}
And then I did the following with hooks:
const [modalIsOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
const targetRef = useRef();
const showTargetElement = () => {
disableBodyScroll(targetRef);
};
const hideTargetElement = () => {
enableBodyScroll(targetRef);
};
useEffect(() => {
if (modalIsOpen === true) {
showTargetElement();
} else {
hideTargetElement();
}
}, [modalIsOpen]);
I don't know if I did it correctly with useRef and useEffect, but it worked, but I can't imagine how I'm going to get to my componentWillUnmount to call mine:
clearAllBodyScrollLocks ();
The basic equivalents for componentDidMount and componentWillUnmount in React Hooks are:
//componentDidMount
useEffect(() => {
doSomethingOnMount();
}, [])
//componentWillUnmount
useEffect(() => {
return () => {
doSomethingOnUnmount();
}
}, [])
These can also be combined into one useEffect:
useEffect(() => {
doSomethingOnMount();
return () => {
doSomethingOnUnmount();
}
}, [])
This process is called effect clean up, you can read more from the documentation.
Related
I have got Component with scroll bar inside it. I would like to know when the scroll bar reaches the top of the component.
Please if anybody can guide me...
Created an example for you on codesandbox
Simplified example:
function Component() {
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
const handleScroll = (e) => {
if (element.scrollTop === 0) {
console.log("do something");
// do whatever you want here
}
};
element.addEventListener("scroll", handleScroll);
return () => element.removeEventListener("scroll", handleScroll);
}, []);
return (
<div ref={ref}></div>
);
}
You can also make a hook out of it if you want to.
You can use the scroll event. No need for ref.
class MyComponent extends React.Component {
handleScroll=(evt)=> { // use this of object instance
if(!evt.currentTarget.scrollTop) {
}
}
render() {
return '<div onScroll={this.handleScroll}></div>';
}
}
I am trying to make a hook that returns the clientX and clientY values when the mouse moves on the screen. My hook looks like this -
useMouseMove hook
const useMouseMove = () => {
const [mouseData, setMouseData] = useState<[number, number]>([0, 0])
useEffect(() => {
const handleMouse = (e: MouseEvent) => {
setMouseData([e.clientX, e.clientY])
}
document.addEventListener("mousemove", handleMouse)
return () => {
document.removeEventListener("mousemove", handleMouse)
}
}, [])
return mouseData
}
And I'm using it in another component like so,
Usage in component
const SomeComponent = () => {
const mouseData = useMouseMoveLocation()
console.log("Rendered") // I want this to be rendered only once
useEffect(() => {
// I need to use the mouseData values here
console.log({ mouseData })
}, [mouseData])
return <>{/* Some child components */}</>
}
I need to use the mouseData values from the useMouseMove hook in the parent component (named SomeComponent in the above example) without re-rendering the entire component every time the mouse moves across the screen. Is there a correct way to do this to optimise for performance?
If you're not going to be rendering this component, then you can't use a useEffect. useEffect's only get run if your component renders. I think you'll need to run whatever code you have in mind in the mousemove callback:
const useMouseMove = (onMouseMove) => {
useEffect(() => {
document.addEventListener("mousemove", onMouseMove)
return () => {
document.removeEventListener("mousemove", onMouseMove)
}
}, [onMouseMove])
}
const SomeComponent = () => {
useMouseMove(e => {
// do something with e.clientX and e.clientY
});
}
I'm trying to update a React Component (B) that renders an SVG object passed from a parent Component (A).
B then uses getSVGDocument().?getElementById("groupID") and adds handling for events based on members of the SVG group.
A also passes in a prop that indicates mouseover in a separate menu.
Simplified code in B:
export function ComponentB(props: {
overviewSvg: string
highlightKey?: string
}) {
function getElems(): HTMLElement[] {
let innerElems = new Array<HTMLElement>()
const svgObj: HTMLObjectElement = document.getElementById(
"my_svg"
) as HTMLObjectElement
if (svgObj) {
const elemGroup = svgObj.getSVGDocument()?.getElementById("elemGroup")
if (elemGroup) {
for (let i = 0; i < elemGroup.children.length; i++) {
innerElems.push(elemGroup.children[i] as HTMLElement)
}
}
}
return innerElems
}
const elems = getElems() // Also tried variations with useState and useEffect, can't seem to get the right combination...
useEffect(() => {
console.log("effect called")
console.log(elems)
elems?.forEach((elem) => {
elem.onmousedown = () => toggleColor(elem)
})
}, [elems, props.overviewSvg])
useEffect(() => {
elems?.forEach((elem) => {
if (elem.id === props.highlightKey) {
setActive(elem)
} else {
setInactive(elem)
}
})
}, [elems, props.highlightKey, props.overviewSvg])
return (
<>
<object data={props.overviewSvg} id="my_svg" />
</>
)
}
I'm not sure what the appropriate parameters for the dependencies in useEffect should be, or if this is entirely the wrong approach?
The console shows that on loading, the array of elems is often empty, in which case the onmousedown loop of course doesn't add any functions.
After the parent sets the props.highlightKey, the second function is triggered and that then triggers the first effect and adds the mousedown functions.
What should I be changing to make the component correctly render before any parent highlightKey changes?
I was able to get the desired behaviour by using useRef and applying it to the DOM element, then adding an onload behaviour to that element.
// Get a ref for the <object> element
const svgObj = useRef<HTMLObjectElement>(null)
const [elemArr, setElems] = useState(Array<HTMLElement>())
if (svgObj.current) {
svgObj.current.onload = () => {
const elems = getElems()
elems.forEach((elem) => {
elem.onmousedown = () => toggleColor(elem)
})
setElems(elems)
}
}
useEffect(() => {
elemArr.forEach((elem) => {
if (elem.id === props.highlightKey) {
setActive(elem)
} else {
setInactive(elem)
}
})
}, [elemArr, props.highlightKey])
return (
<object ref={svgObj} data={props.overviewSVG} ... />
)
I'm creating a custom hook to detect clicks inside/outside a given HTMLElement.
Since the hook accepts a function as an argument, it seems like either the input needs to be wrapped in a useCallback or stored inside the hook with useRef to prevent useEffect from triggering repeatedly.
Are both of the following approaches functionally the same?
Approach One (preferred)
// CALLER
useClickInsideOutside({
htmlElement: htmlRef.current,
onClickOutside: () => {
// Do something via anonymous function
},
});
// HOOK
const useClickInsideOutside = ({
htmlElement,
onClickOutside,
}) => {
const onClickOutsideRef = useRef(onClickOutside);
onClickOutsideRef.current = onClickOutside;
useEffect(() => {
function handleClick(event) {
if (htmlElement && !htmlElement.contains(event.target)) {
onClickOutsideRef.current && onClickOutsideRef.current();
}
}
document.addEventListener(MOUSE_DOWN, handleClick);
return () => { document.removeEventListener(MOUSE_DOWN, handleClick); };
}, [htmlElement]);
}
Approach Two
// CALLER
const onClickOutside = useCallback(() => {
// Do something via memoized callback
}, []);
useClickInsideOutside({
htmlElement: htmlRef.current,
onClickOutside,
});
// HOOK
const useClickInsideOutside = ({
htmlElement,
onClickOutside,
}) => {
useEffect(() => {
function handleClick(event) {
if (htmlElement && !htmlElement.contains(event.target)) {
onClickOutside();
}
}
document.addEventListener(MOUSE_DOWN, handleClick);
return () => { document.removeEventListener(MOUSE_DOWN, handleClick); };
}, [htmlElement, onClickOutside]);
}
Does the first one (which I prefer, because it seems to make the hook easier to use/rely on fewer assumptions) work as I imagine? Or might useEffect suffer from enclosing stale function references inside handleClick?
useCallback is the right approach for this. However, I think I'd design it in a way where I could abstract the memo-ing away from the consumer:
/**
* #param {MutableRefObject<HTMLElement>} ref
* #param {Function} onClickOutside
*/
const useClickInsideOutside = (ref, onClickOutside) => {
const { current: htmlElement } = ref;
const onClick = useCallback((e) => {
if (htmlElement && !htmlElement.contains(e.target)) {
onClickOutside();
}
}, [htmlElement, onClickOutside])
useEffect(() => {
document.addEventListener(MOUSE_DOWN, onClick);
return () => document.removeEventListener(MOUSE_DOWN, onClick);;
}, [onClick]);
}
And since I already touched on design, I'd try to make the design resemble similar API functions where 2 arguments are [in]directly related. I'd end up looking like this:
// Consumer component
const ref = useRef()
useClickOutside(ref, () => {
// do stuff in here
})
I tried adding the condition on mouseenter and mouseleave however the modal is not working but when I tried to create a button onClick={() => {openModal();}} the modal will show up. Can you please tell me what's wrong on my code and which part.
const openModal = event => {
if (event) event.preventDefault();
setShowModal(true);
};
const closeModal = event => {
if (event) event.preventDefault();
setShowModal(false);
};
function useHover() {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
return () => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
};
}, [ref]);
return [ref, hovered];
}
const [ref, hovered] = useHover();
<div className="hover-me" ref={ref}>hover me</div>
{hovered && (
<Modal active={showModal} closeModal={closeModal} className="dropzone-modal">
<div>content here</div>
</Modal>
)}
building on Drew Reese's answer, you can cache the node reference inside the useEffect closure itself, and it simplifies things a bit. You can read more about closures in this stackoverflow thread.
const useHover = () => {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
const el = ref.current; // cache external ref value for cleanup use
if (el) {
el.addEventListener("mouseenter", enter);
el.addEventListener("mouseleave", leave);
return () => {
el.removeEventLisener("mouseenter", enter);
el.removeEventLisener("mouseleave", leave);
};
}
}, []);
return [ref, hovered];
};
I almost gave up and passed on this but it was an interesting problem.
Issues:
The first main issue is with the useEffect hook of your useHover hook, it needs to add/remove both event listeners at the same time, when the ref's current component mounts and unmounts. The key part is the hook needs to cache the current ref within the effect hook in order for the cleanup function to correctly function.
The second issue is you aren't removing the listener in the returned effect hook cleanup function.
The third issue is that EventTarget.addEventListener() returns undefined, which is a falsey value, thus your hook never calls modalOpen or modalClose
The last issue is with the modal open/close state/callbacks being coupled to the useHover hook's implementation. (this is fine, but with this level of coupling you may as well just put the hook logic directly in the parent component, completely defeating the point of factoring it out into a reusable hook!)
Solution
Here's what I was able to get working:
const useHover = () => {
const ref = useRef();
const _ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current) {
_ref.current = ref.current; // cache external ref value for cleanup use
ref.current.addEventListener("mouseenter", enter);
ref.current.addEventListener("mouseleave", leave);
}
return () => {
if (_ref.current) {
_ref.current.removeEventLisener("mouseenter", enter);
_ref.current.removeEventLisener("mouseleave", leave);
}
};
}, []);
return [ref, hovered];
};
Note: using this with a modal appears to have interaction issues as I suspected, but perhaps your modal works better.