window.visualViewport.height doenst update useEffect when a dependency - reactjs

This ugly code works. Every second viewportHeight is set to the value of window.visualViewport.height
const [viewportHeight, setViewportHeight] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setViewportHeight(window.visualViewport.height);
}, 1000);
}, []);
However this doesn't work. viewportHeight is set on page load but not when the height changes.
React.useEffect(() => {
setViewportHeight(window.visualViewport.height);
}, [window.visualViewport.height]);
Additional context: I need the page's height in state and I need the virtual keyboard's height to be subtracted from this on Mobile iOS.

You can only use state variables managed by React as dependencies - so a change in window.visualViewport.height will not trigger your effect.
You can instead create a div that spans the whole screen space and use a resize observer to trigger effects when its size changes:
import React from "react";
import useResizeObserver from "use-resize-observer";
const App = () => {
const { ref, width = 0, height = 0 } = useResizeObserver();
const [viewportHeight, setViewportHeight] = React.useState(height);
React.useEffect(() => {
setViewportHeight(window.visualViewport.height);
}, [height]);
return (
<div ref={ref} style={{ width: "100vw", height: "100vh" }}>
// ...
</div>
);
};

This custom hook works:
function useVisualViewportHeight() {
const [viewportHeight, setViewportHeight] = useState(undefined);
useEffect(() => {
function handleResize() {
setViewportHeight(window.visualViewport.height);
}
window.visualViewport.addEventListener('resize', handleResize);
handleResize();
return () => window.visualViewport.removeEventListener('resize', handleResize);
}, []);
return viewportHeight;
}

Related

How to imitate hover and active effects onkeydown in react?

Edit:
I have an example on codepen showing the effects I want to implement. It is working, but I'm not sure if it is cleanly done to satisfy React 18's StrictMode.
https://codepen.io/yochess/pen/NWMOvrv?editors=0110
Question:
I have code where I have 2 div elements representing a left arrow and a right arrow.
When I hover over and click the arrow div element, I have css code that changes their styling using hover and active.
//file.css
.left-arrow-wrapper:active {
color: black;
}
.left-arrow-wrapper:hover{
background: rgb(254 226 226) !important;
}
.right-arrow-wrapper:active {
color: black;
}
.right-arrow-wrapper:hover{
background: rgb(254 226 226) !important;
}
I want to sort of emulate this effect with onkeydown.
For example, if the left arrow is clicked then setState is invoked on the left arrow's stylings. 0.1 seconds later using setTimeout, a 2nd setState is invoked on the left arrow and its styling would revert back to its original state.
As a result, useEffect is visited a few times. I want to make sure if a user is spamming the left arrow, then while its styling is changed, I want no effects to take place.
I am new to React and Hooks and the code above works on React 17, but when I change to React 18, the code is bugged. I'm assuming I am incorrectly implementing this effect. Is there a more proper way to accomplish this?
A side question is how do I properly clean up the setTimeouts on unmount? In the code below, I push all the setTimeouts into an array, and then set them to null once they are called. Then on unmount, I would return a function that clears the setTimeouts that are not null.
Here is the code:
import React, { useState, useEffect, useRef } from 'react'
import {
FaAngleLeft,
FaAngleRight,
} from "react-icons/fa"
const setStyles = {
defaultArrowStyle:{
backgroundColor: "lightgray",
borderStyle: "ridge"
},
clickedArrowStyle: {
backgroundColor: "rgb(254 226 226)",
borderStyle: "ridge"
},
}
const ArrowButtons = () => {
const [leftArrowStyle, setLeftArrowStyle] = useState(setStyles.defaultArrowStyle)
const [rightArrowStyle, setRightArrowStyle] = useState(setStyles.defaultArrowStyle)
const timeRef = useRef([])
const counterRef = useRef(0)
useEffect(() => {
window.addEventListener("keydown", handleKey)
return () => {
window.removeEventListener("keydown", handleKey);
}
}, [handleKey])
useEffect(() => {
if (rightArrowStyle.backgroundColor !== setStyles.clickedArrowStyle.backgroundColor) return
const counter = counterRef.current
const timer = setTimeout(() => cacheAndSetArrowStyle(setRightArrowStyle, counter), 100)
timeRef.current[counterRef.current++] = timer
}, [rightArrowStyle])
useEffect(() => {
if (leftArrowStyle.backgroundColor !== setStyles.clickedArrowStyle.backgroundColor) return
const counter = counterRef.current
const timer = setTimeout(() => cacheAndSetArrowStyle(setLeftArrowStyle, counter), 100)
timeRef.current[counterRef.current++] = timer
}, [leftArrowStyle])
useEffect(() => {
return () => {
// eslint-disable-next-line
timeRef.current.filter(timer => timer).forEach(timer => clearTimeout(timer))
}
}, [])
return (
<div className="row">
<div className="col-2 hand-icon text-center left-arrow-wrapper" style={leftArrowStyle} onClick={handleLeftClick}>
<FaAngleLeft className="left-arrow" />
</div>
<div className="col-2 hand-icon text-center right-arrow-wrapper" style={rightArrowStyle} onClick={handleRightClick}>
<FaAngleRight className="double-right-arrow" />
</div>
</div>
)
function handleKey(event) {
if (event.key === "ArrowRight") {
setRightArrowStyle(setStyles.clickedArrowStyle)
}
if (event.key === "ArrowLeft") {
setLeftArrowStyle(setStyles.clickedArrowStyle)
}
}
function cacheAndSetArrowStyle(setArrowStyle, counter) {
timeRef.current[counter] = null
setArrowStyle(setStyles.defaultArrowStyle)
}
}
export default React.memo(ArrowButtons)

I want to change style according to window width using states

style={window.innerWidth<='750 px' ?{myStyle}:{}}
can anyone help me in this?
I want to set the style according to window width.
myStyle is a state.
Hey here is a hook I wrote awhile back for getting the window dimensions then you can just use the width from the hook in your style tag with a ternary operator to display different styles
function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window;
return {
width,
height,
};
}
/**
* #returns {object} {width, height}
*/
export default function useWindowDimensions() {
const [windowDimensions, setWindowDimensions] = useState(
getWindowDimensions()
);
useEffect(() => {
function handleResize() {
setWindowDimensions(getWindowDimensions());
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowDimensions;
}
usage
const Component = () => {
const {width, height} = useWindowDimensions()
return <div style ={width > 750 ? {Place >750 styles here} : {Place < 750 styles here} }></div>
}
The best way to do that is using a custom hook. This hook can get the current window width and height when the window size is changed.
The event listener is registered when the component is mounted to update the state and safely removed when it is unmounted.
import { useState, useEffect } from "react";
// hook to get window size dynamically
const useWindowSize = () => {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined
});
useEffect(() => {
// only execute all the code below in client side
if (typeof window !== "undefined") {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
};
export default useWindowSize;
This solution works perfectly with server rendering as well. Import the useWindowSize hook into the component you want and use the size as you want.
import useWindowSize from '/path/to/hooks/useWindowSize'
const MyComp = () => {
const size = useWindowSize();
return <div style={style={size.width<='750 px' ?{myStyle}:{}}}></div>;
};
export default MyComp;

Using cleanup function in React UseEffect

In my app there is a navbar that pops down after the user scrolled to a certain point. I use two separate navbars and define the current scroll position like this:
const newNavbar = () => {
if (window !== undefined) {
let posHeight_2 = window.scrollY;
posHeight_2 > 112 ? setNewNav(!newNav) : setNewNav(newNav)
}
};
const stickNavbar = () => {
if (window !== undefined) {
let windowHeight = window.scrollY;
windowHeight > 150 ? setSticky({ position: "fixed", top: "0", marginTop:"0", transition: "top 1s"}) : setSticky({});
}
};
const scrollPos = () => {
if (window !== undefined) {
let posHeight = window.scrollY;
posHeight > 112 ? setScroll(posHeight) : setScroll(0)
}
};
Current states are managed by useState and given to a class, which is triggered by the changing scroll position:
const [scroll, setScroll] = useState(0);
const [newNav, setNewNav] = useState (false)
const [sticky, setSticky] = useState({});
const navClass = newNav ? 'menu-2 show' : 'menu-2'
<Navbar className={navClass}>
//
</Navbar>
finally UseEffect to make use of the states:
React.useEffect(() => {
window.addEventListener('scroll', stickNavbar);
return () => window.removeEventListener('scroll', stickNavbar);
}, []);
React.useEffect(() => {
window.addEventListener('scroll', scrollPos);
return () => window.removeEventListener('scroll', stickNavbar);
}, []);
React.useEffect(() => {
window.addEventListener('scroll', newNavbar);
return () => window.removeEventListener('scroll', newNavbar);
}, []);
However my cleanup functions are not working, I get the error Warning: Can't perform a React state update on an unmounted component.
Your second useEffect contains a copy/paste error.
It should remove scrollPos (since that's what you bound), not stickNavbar.
Because of this scrollPos listener is not removed, which causes an error on the next scroll event, as the bound function no longer exists after the component is removed from DOM.

How to preserve aspect ratio upon resizing browser containing a react-lottie component?

I have a Lottie animation that is rendered using react-lottie (https://www.npmjs.com/package/react-lottie)
import Lottie from 'react-lottie'
import animationData from './path/to/animation.json'
const MyComponent = () => {
const myOptions = {
loop: true,
autoplay: true,
renderer: 'canvas',
animationData: animationData
}
return (
<div>
...other stuff
<Lottie options={myOptions} width={1000} height={500} />
</div>
)
}
The animation displays and plays fine.
The issue is that when resizing the browser, the animation shrinks and squishes and does not preserve its original aspect ratio. If I do a page refresh after shrinking the browser, the animation is then rendered correctly. It is only when the resizing is actively occurring that the aspect ratio is off.
I have tried adding:
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice' // also tried 'xMidYMid meet'
}
to my options but that does not work either.
The only other thing I can think of is to attach a resize event listener to the window which will in turn call lottie.resize()
useEffect(() => {
window.addEventListener('resize', ??)
}, [])
The stuff in ?? is where I get stuck. I know that using lottie-web, I have direct access to a lottie.resize() function. However, when using react-lottie, how do I call/access the resize function?
Any help would be greatly appreciated.
Edit: I have been asked to add an example animation JSON so it is here: https://pastebin.com/gXCEhKaR
Yes, thank you, I started it at home, but you have the width and height values set for the Lottie component. If you want Lottie to change its size depending on the size of the screen, then you need to add a hook to change the screen.
Hook resize
export function useSizeComponents (ref) {
const [size, setSize] = useState([0, 0])
useLayoutEffect(() => {
function updateSize () {
let newSize = [window.innerWidth, window.innerHeight]
if (ref?.current) {
newSize = [ref.current.offsetWidth, ref.current.offsetHeight]
}
setSize(newSize)
}
window.addEventListener('resize', updateSize)
updateSize()
return () => window.removeEventListener('resize', updateSize)
}, [])
return size
}
Your component
const MyComponent = () => {
const [width, height] = useSizeComponents()
const scaleLottie = 0.5
const myOptions = {
loop: true,
autoplay: true,
renderer: 'canvas',
animationData: animationData,
}
const control = useMemo(() => {
if (!width) return null
const xMidYMid = 0.5
const sizeComponent = {
width: width * scaleLottie,
height: width * scaleLottie * xMidYMid
}
return <Lottie key={width} options={myOptions} {...sizeComponent} />
}, [width])
return (
<div>
...other stuff
{control}
</div>
)
}

Increase Value with useState using ReactJS Hooks

Hi I am new developer at ReactJs world. I have a question. I have value variable with initial value as 1. But I have a problem while increasing it. In JavaScript I can incarease an any value one by one but I did not make same thing using Hooks. The thing which I want to do is changing background image with time. Could you help me at this issue? How can I change my background image with time ?
my example tsx.part:
import React, { useEffect, useState } from 'react';
const LeftPart = (props: any) => {
let imgNumber : number = 1;
const [value, setValue] = useState(1);
useEffect(() => {
const interval = setInterval(() => {
imgNumber = imgNumber + 1;
setValue(value+1);
console.log(imgNumber)
console.log(value)
}, 3000);
return () => clearInterval(interval);
}, []);
return (
<div className="col-xl-7 col-lg-7 col-md-7 col-sm col-12">
<img id="image" src={"../../../assets/images/bg"+{value}+".jpg"} style={{ width: "100%", height: "99vh" }} alt="Login Images"></img>
</div >
)
}
export default LeftPart;
Your issue is the useEffect block dependency list (that empty array). When you explicitly set no dependencies, React will call the callback on first render and never again. If you want it to continuously change, just remove that second parameter entirely. If you implicitly leave no useEffect dependencies, it is called on every render.
Fixed:
useEffect(() => {
const interval = setInterval(() => {
imgNumber = imgNumber + 1;
setValue(value+1);
console.log(imgNumber)
console.log(value)
}, 3000);
return () => clearInterval(interval);
});

Resources