I'm creating a custom React hook. The scrolling target element can either be the myref.current or the window object.
If it's the myref.current target value I want to use the scrollTop method, if it's the window object I want to use pageYOffset method (since scrollTop method isn't available for the window).
How do I create this function and write this in a clean way?
const useMyHook = (target: 'ref' | 'window') => {
const [fade, setFade] = useState(false);
const myRef = useRef<HTMLDivElement>(null);
// useEffect(() => {
// const onScroll = () => {
// if (window.pageYOffset > 50) {
// setFade(true);
// } else {
// setFade(false);
// }
// };
// window.addEventListener('scroll', onScroll);
// return () => window.removeEventListener('scroll', onScroll);
// }, []);
useEffect(() => {
const node = target === 'ref' ? scrollRef.current : window;
const onScroll = () => {
if (node) {
if (node.scrollTop > 50) {
setFade(true);
} else {
setFade(false);
}
}
};
if (node) {
node.addEventListener('scroll', onScroll);
}
return () => node?.removeEventListener('scroll', onScroll);
}, []);
return { myRef, fade };
};
export default useMyHook;
I would have implemented this way:
// node - any element you want to add event listener to, For eg: scrollRef.current or the window object
const useMyHook = (node) => {
const [fade, setFade] = useState(false);
useEffect(() => {
if (!node) return
const onScroll = () => {
const verticalScroll = node === window ? node.pageYOffset : node.scrollTop;
if (verticalScroll > 50) {
setFade(true);
} else {
setFade(false);
}
};
node.addEventListener('scroll', onScroll);
return () => node.removeEventListener('scroll', onScroll);
}, [node]);
return fade;
};
export default useMyHook;
PS: Not sure why you used myRef.
This is the cleanest in my opinion (also removed myRef as its not in used):
const useMyHook = node => {
const [fade, setFade] = useState(false);
useEffect(() => {
if (node){
const verticalScroll = node === window ? node.pageYOffset : node.scrollTop;
const onScroll = () => setFade(verticalScroll > 50);
node.addEventListener('scroll', onScroll);
}
return () => node?.removeEventListener('scroll', onScroll);
}, [node]);
return fade;
};
export default useMyHook;
Related
When running the below code that gets the screen width of the browser I get this error:
ReferenceError: window is not defined
The code commented-out works, uses 0 as a default, but correctly updates when the browser width is changed.
I either get the window is not defined error or pass a default number to state, which displays the incorrect width on page load.
How do I define window.innerWidth at page load?
import React, { useState, useEffect } from "react";
const WindowTracker = () => {
const [show, setShow] = useState(true);
// const [windowWidth, setWindowWidth] = useState(0);
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const triggerToggle = () => {
setShow(!show);
};
useEffect(() => {
function watchWidth() {
setWindowWidth(window.innerWidth);
}
window.addEventListener("resize", watchWidth);
return function () {
window.removeEventListener("resize", watchWidth);
};
}, []);
return (
<div>
<button onClick={triggerToggle}>Toggle WindowTracker</button>
{show && <h1>Window width: {windowWidth}px</h1>}
</div>
);
// }
};
export default WindowTracker;
Thanks!
This happens because in the pre rendering that nextjs performs on the window server it is not yet defined, I solved this in my code by adding an if, this is an example code of how it would look
import React, { useState, useEffect } from "react";
const WindowTracker = () => {
const [show, setShow] = useState(true);
// const [windowWidth, setWindowWidth] = useState(0);
const [windowWidth, setWindowWidth] = useState(typeof window !== "undefined" ? window.innerWidth : 0);
const triggerToggle = () => {
setShow(!show);
};
useEffect(() => {
if (typeof window !== "undefined") {
function watchWidth() {
setWindowWidth(window.innerWidth);
}
window.addEventListener("resize", watchWidth);
return function () {
window.removeEventListener("resize", watchWidth);
};
}
}, []);
return (
<div>
<button onClick={triggerToggle}>Toggle WindowTracker</button>
{show && <h1>Window width: {windowWidth}px</h1>}
</div>
);
// }
};
export default WindowTracker;
Use this Hook :
const [windowWidth, setWindowWidth] = useState();
useEffect(() => {
setWindowWidth(window.innerWidth)
}, [window.innerWidth])
You should trigger setWindowWidth when component did mount
const isClientSide = () => typeof window !== 'undefined';
const getWindowSize = () => {
if (isClientSide()) return window.innerWidth
return 0
}
const WindowTracker = () => {
const [show, setShow] = useState(true);
const [windowWidth, setWindowWidth] = useState(getWindowSize());
const triggerToggle = () => {
setShow(!show);
};
useEffect(() => {
function watchWidth() {
setWindowWidth(window.innerWidth);
}
window.addEventListener("resize", watchWidth);
setWindowSize(getWindowSize()); <=== This should set the right width
return function () {
window.removeEventListener("resize", watchWidth);
};
}, []);
return (
<div>
<button onClick={triggerToggle}>Toggle WindowTracker</button>
{show && <h1>Window width: {windowWidth}px</h1>}
</div>
);
};
export default WindowTracker;
Inside useEffect hook you can also invoke setWindowWidth(window.innerWidth); at to set the state with window.innerWidth to have desired width on pageLoad.
const [windowWidth, setWindowWidth] = useState(0);
useEffect(() => {
function watchWidth() {
setWindowWidth(window.innerWidth);
}
window.addEventListener("resize", watchWidth);
// Call right away so state gets updated with initial window size
setWindowWidth(window.innerWidth);
return function () {
window.removeEventListener("resize", watchWidth);
};
}, []);
I'm trying a project where I use my handpose to play the dino game in chrome. It's been 6 hours and I cannot seem to find a good solution to passing props to the game. Here is the code of the App.js
function App() {
const [isTouching, setIsTouching] = useState(false);
const webcamRef = useRef(null);
const canvasRef = useRef(null);
const runHandpose = async () => {
const net = await handpose.load();
console.log('Handpose model loaded.');
// Loop and detect hands
setInterval(() => {
detect(net);
}, 100)
};
const detect = async (net) => {
if (
typeof webcamRef.current !== 'undefined' &&
webcamRef.current !== null &&
webcamRef.current.video.readyState === 4
) {
...
await handleDistance(hand);
}
}
const handleDistance = (predictions) => {
if (predictions.length > 0) {
predictions.forEach(async (prediction) => {
const landmarks = prediction.landmarks;
const thumbTipPoint = landmarks[4]
const indexFingerTipPoint = landmarks[8]
const xDiff = thumbTipPoint[0] - indexFingerTipPoint[0]
const yDiff = thumbTipPoint[1] - indexFingerTipPoint[1]
const dist = Math.sqrt(xDiff*xDiff + yDiff*yDiff)
if (dist < 35) {
setIsTouching(true);
} else {
setIsTouching(false);
}
})
}
}
useEffect(() => {
console.log(isTouching)
}, [isTouching])
useEffect(() => {
runHandpose();
}, [])
return (
<div className="App">
<div className="App-header">
<Webcam ref={webcamRef}
style={{...}}/>
<canvas ref={canvasRef}
style={{...}} />
</div>
<Game isTouching={isTouching} />
</div>
);
}
And here is some code from the game
export default function Game({ isTouching }) {
const worldRef = useRef();
const screenRef = useRef();
const groundRef1 = useRef();
const groundRef2 = useRef();
const dinoRef = useRef();
const scoreRef = useRef();
function setPixelToWorldScale() {
...
}
function handleStart() {
lastTime = null
speedScale = 1
score = 0
setupGround(groundRef1, groundRef2)
setupDino(dinoRef, isTouching)
setupCactus()
screenRef.current.classList.add("hide")
window.requestAnimationFrame(update)
}
async function update(time) {
if (lastTime == null) {
lastTime = time
window.requestAnimationFrame((time) => {
update(time)
})
return
}
const delta = time - lastTime
updateGround(delta, speedScale, groundRef1, groundRef2)
updateDino(delta, speedScale, dinoRef)
updateCactus(delta, speedScale, worldRef)
updateSpeedScale(delta)
updateScore(delta, scoreRef)
// if (checkLose()) return handleLose(dinoRef, screenRef)
lastTime = time
window.requestAnimationFrame((time) => {
update(time)
})
}
function updateSpeedScale(delta) {...}
function updateScore(delta) {...}
function checkLose() {...}
function isCollision(rect1, rect2) {...}
useEffect(() => {
console.log(isTouching)
window.requestAnimationFrame(update);
}, [isTouching])
function handleLose() {...}
useEffect(() => {
setPixelToWorldScale()
window.addEventListener("resize", setPixelToWorldScale())
document.addEventListener("click", handleStart, { once: true })
},[])
return (...);
}
What I've been trying to do is how I can pass isTouching to the game everytime my thumb and my index finger meet. But I want to avoid re-render the game and I only want to update the dino. But I cannot find a way to do that. I was also trying to create a isJump state inside the game but I don't know how to pass the setisJump to the parent (which is App) so that I can do something like this:
useEffect(() => {
setIsJump(true)
}, [isTouching])
Does anyone have a better idea of how to do this? Or did I made a mistake on passing the props here? Thank you
I want to make some changes depending on the screen size
useEffect(() => {
console.log("am called");
if (window.innerWidth < 800) {
document.getElementsByTagName("body")[0].style.display = "none";
}
if (window.innerWidth > 800) {
document.getElementsByTagName("body")[0].style.display = "block";
}
}, [window.innerWidth]);
what is the problem with this code
You need to observe window size changing and put this value to state.
const [width, setWidth] = useState(0)
useEffect(() => {
const onResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener("resize", onResize)
}
}, [setWidth])
//and then
useEffect(() => {
console.log("am called");
if (width< 800) {
document.getElementsByTagName("body")[0].style.display = "none";
} else {
document.getElementsByTagName("body")[0].style.display = "block";
}
}, [width]);
better solution
export default function useMatchMedia(condition) {
const [isMatching, setIsMatching] = useState(false);
useEffect(
function updateMatchMedia() {
if (!condition) return setIsMatching(true);
const updateIsMatching = (event) => setIsMatching(event.matches);
const matcher = window.matchMedia(condition);
setIsMatching(matcher.matches);
matcher.addEventListener('change', updateIsMatching);
return () => {
matcher.removeEventListener('change', updateIsMatching);
};
},
[condition]
);
return [isMatching];
}
then you can use it
isTablet = useMatchMedia(`(max-width: 800px)`);
useEffect(() => {
if(isTablet){
} else {
}
}, [isTablet])
I am trying to implement a onWheel triggered Nav for a carousel. The button nav works, while the onWheel triggers, but is somehow not accessing the initialized state of the context provider. Any insight would be appreciated. thank you.
context provider:
import CarouselContext from "../context/_carousel"
const CarouselProvider = ({ children }) => {
const [isInitialized, setIsInitialized] = useState(false)
const [carouselLength, setCarouselLength] = useState(0)
const [carouselPosition, setCarouselPosition] = useState(0)
const initializeCarousel = length => {
setCarouselLength(length)
setIsInitialized(true)
console.log(`carouselLength ${carouselLength}`)
}
const nextSlide = () => {
if (carouselPosition === carouselLength) {
setCarouselPosition(0)
} else {
setCarouselPosition(carouselPosition + 1)
}
console.log(`carouselPosition ${carouselPosition}`)
}
const previousSlide = () => {
if (carouselPosition === 0) {
setCarouselPosition(carouselLength)
} else {
setCarouselPosition(carouselPosition - 1)
}
console.log(`carouselPosition ${carouselPosition}`)
}
const state = { carouselLength, carouselPosition, isInitialized }
const methods = { initializeCarousel, nextSlide, previousSlide }
return (
<CarouselContext.Provider value={[state, methods]}>
{children}
</CarouselContext.Provider>
)
}
export default CarouselProvider
carousel structure:
return (
<Page className="works">
<CarouselProvider>
<ScrollNav>
<PreviousWorkButton />
<Carousel>
{works.map((work, index) => (
<CarouselItem key={index}>
<Work project={work} />
</CarouselItem>
))}
</Carousel>
<NextWorkButton />
</ScrollNav>
</CarouselProvider>
</Page>
)
scroll Nav (which is consoling the events are triggered, but not showing the current position of the carousel or length)
const ScrollNav = ({ children }) => {
const [, { nextSlide, previousSlide }] = useContext(CarouselContext)
const delayedScroll = useCallback(
debounce(e => changeNav(e), 500, { leading: true, trailing: false }),
[]
)
const changeNav = direction => {
if (direction === 1) {
nextSlide()
}
if (direction === -1) {
previousSlide()
}
}
const onWheel = e => {
delayedScroll(e.deltaY)
}
return <div onWheel={onWheel}>{children}</div>
}
onclick button that triggers the same event with carousel position and length persisting
const NextWorkButton = () => {
const [, { nextSlide }] = useContext(CarouselContext)
const clicked = () => {
nextSlide()
}
return (
<div className="next-work-button">
<button onClick={clicked}>
<DownArrowSvg />
</button>
</div>
)
}
edited to add console.logs in the provider as is in my local copy
console logs on click event:
carouselPosition 1
carouselLength 5
console log on wheel event (the length does not print):
carouselPosition 0
Thanks to kumarmo2 I solved this by removing the debounce and calling the event directly. I made a very hacky debounce specific to the wheel event with a timer.
my solution:
const ScrollNav = ({ children }) => {
const [, { nextSlide, previousSlide }] = useContext(CarouselContext)
const [debounce, setDebounce] = useState(false)
const timer = () => {
setTimeout(() => {
setDebounce(false)
}, 1000)
}
const changeNav = e => {
let direction = e.deltaY
if (debounce) {
return
} else if (direction >= 1) {
setDebounce(true)
timer()
nextSlide()
return
} else if (direction <= -1) {
setDebounce(true)
timer()
previousSlide()
return
}
}
return <div onWheel={changeNav}>{children}</div>
}
I think what is happening here is that, your delayedScroll is not getting updated because of useCallback. It "captures" the changeNav which in turn captures the nextSlide.
So nextSlide will be called, but since its references inside delayedScroll is not updated because of useCallback, you are facing the issue.
Can you try removing the useCallback and debounce once for delayedScroll ? and if it works, will introduce the debounce logic in the correct way.
I get the console log to fire inside the hook on each resize however the console log that's in the callback that gets passed into the resize hook never fires.
Here is the hook i have which uses useCallbacks per the exhaustive deps eslint plugin:
// #flow
import { useCallback, useEffect } from 'react';
let resizeId: AnimationFrameID;
export const useResize = (callbackFn: Function, ref?: any) => {
const resizeUpdate = useCallback(() => {
console.log('inside');
callbackFn();
}, [callbackFn]);
const handleResize = useCallback(() => {
cancelAnimationFrame(resizeId);
resizeId = requestAnimationFrame(resizeUpdate);
}, [resizeUpdate]);
const handleOrientation = useCallback(() => {
// After orientationchange, add a one-time resize event
const afterOrientationChange = () => {
handleResize();
// Remove the resize event listener after it has executed
window.removeEventListener('resize', afterOrientationChange);
};
window.addEventListener('resize', afterOrientationChange);
}, [handleResize]);
useEffect(() => {
if (typeof ResizeObserver === 'function' && ref && ref.current) {
let resizeObserver = new ResizeObserver(() => handleResize());
resizeObserver.observe(ref.current);
return () => {
if (!resizeObserver) {
return;
}
resizeObserver.disconnect();
resizeObserver = null;
};
} else {
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleOrientation);
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleOrientation);
cancelAnimationFrame(resizeId);
};
}
}, [callbackFn, handleOrientation, handleResize, ref]);
};
export default useResize;
Here is how you use it within a component:
...
const setContentRef = () => {
console.log('resize');
};
useResize(setContentRef);
...