Vue JS - How to get window size whenever it changes - javascript

I'm new to vuejs but I was trying to get the window size whenever I
resize it so that i can compare it to some value for a function that I
need to apply depending on the screen size. I also tried using the
watch property but not sure how to handle it so that's probably why it didn't work
methods: {
elem() {
this.size = window.innerWidth;
return this.size;
},
mounted() {
if (this.elem < 767){ //some code }
}

Put this code inside your Vue component:
created() {
window.addEventListener("resize", this.myEventHandler);
},
destroyed() {
window.removeEventListener("resize", this.myEventHandler);
},
methods: {
myEventHandler(e) {
// your code for handling resize...
}
}
This will register your Vue method on component creation, trigger myEventHandler when the browser window is resized, and free up memory once your component is destroyed.

For Vue3, you may use the code below:
mounted() {
window.addEventListener("resize", this.myEventHandler);
},
unmounted() {
window.removeEventListener("resize", this.myEventHandler);
},
methods: {
myEventHandler(e) {
// your code for handling resize...
}
}
destroyed and beforeDestroyed is deprecated in Vue3, hence you might want to use the beforeUnmount and unmounted

Simplest approach
https://www.npmjs.com/package/vue-window-size
Preview
import Vue from 'vue';
import VueWindowSize from 'vue-window-size';
Vue.use(VueWindowSize);
You would then access it normally from your components like this:
<template>
<div>
<p>window width: {{ windowWidth }}</p>
<p>window height: {{ windowHeight }}</p>
</div>
</template>

I looked at the code of that library vue-window-size, and besides the additional logic, it's just adding an event listener on window resize, and it looks like it can be instructed to debounce. Source
The critical problem for me is that my Vue SPA app does not emit a window resize event when a vue-router route changes that makes the <html> element go from 1000px to 4000px, so it's causing me all kinds of problems watching a canvas element controlled by p5.js to redraw a wallpaper using p5.resizeCanvas().
I have a different solution now that involves actively polling the page's offset height.
The first thing to be aware of is JavaScript memory management, so to avoid memory leaks, I put setInterval in the created lifecycle method and clearInterval in the beforeDestroy lifecycle method:
created() {
this.refreshScrollableArea = setInterval(() => {
const { offsetWidth, offsetHeight } = document.getElementById('app');
this.offsetWidth = offsetWidth;
this.offsetHeight = offsetHeight;
}, 100);
},
beforeDestroy() {
return clearInterval(this.refreshScrollableArea);
},
As hinted in the above code, I also placed some initial state:
data() {
const { offsetWidth, offsetHeight } = document.querySelector('#app');
return {
offsetWidth,
offsetHeight,
refreshScrollableArea: undefined,
};
},
Note: if you are using getElementById with something like this.id (ie: an element that is a child in this component), document.getElementById(this.id) will be undefined because DOM elements load outer-to-inner, so if you see an error stemming from the data instantiation, set the width/height to 0 initially.
Then, I put a watcher on offsetHeight to listen for height changes and perform business logic:
watch: {
offsetHeight() {
console.log('offsetHeight changed', this.offsetHeight);
this.state = IS_RESET;
this.setState(this.sketch);
return this.draw(this.sketch);
},
},
Conclusion: I tested with performance.now() and:
document.querySelector('#app').offsetHeight
document.getElementById('app').offsetHeight
document.querySelector('#app').getClientBoundingRect().height
all execute in about the exact same amount of time: 0.2ms, so the above code is costing about 0.2ms every 100ms. I currently find that reasonable in my app including after I adjust for slow clients that operate an order of magnitude slower than my localmachine.
Here is the test logic for your own R&D:
const t0 = performance.now();
const { offsetWidth, offsetHeight } = document.getElementById('app');
const t1 = performance.now();
console.log('execution time:', (t1 - t0), 'ms');
Bonus: if you get any performance issue due to long-running execution time on your setInterval function, try wrapping it in a double-requestAnimationFrame:
created() {
this.refreshScrollableArea = setInterval(() => {
return requestAnimationFrame(() => requestAnimationFrame(() => {
const { offsetWidth, offsetHeight } = document.getElementById(this.id);
this.offsetWidth = offsetWidth;
this.offsetHeight = offsetHeight;
}));
}, 100);
},
requestAnimationFrame itself a person should research. I will leave it out of the scope of this answer.
In closing, another idea I researched later, but am not using is to use a recursive setTimeout function with a dynamic timeout on it (ie: a timeout that decays after the page loads); however, if you consider the recursive setTimeout technique, be conscious of callstack/function-queue length and tail call optimization. Stack size could run away on you.

You can use this anywhere anytime
methods: {
//define below method first.
winWidth: function () {
setInterval(() => {
var w = window.innerWidth;
if (w < 768) {
this.clientsTestimonialsPages = 1
} else if (w < 960) {
this.clientsTestimonialsPages = 2
} else if (w < 1200) {
this.clientsTestimonialsPages = 3
} else {
this.clientsTestimonialsPages = 4
}
}, 100);
}
},
mounted() {
//callback once mounted
this.winWidth()
}

Related

Issue accessing state array from inside animationFrame with react

I am trying to update some elements on scroll by using animationFrame. I want to add an easing effect so I would like the elements to update their positions by the eased value. I figured the best way to do this would be to store all of the values in an array and update them accordingly. When each element is mounted I am sending them to a context element that adds them to the state value array.
My issue is that I cannot access the array from inside the animating function. It is available outside of the animating function but not inside. I am assuming that the animation is starting before the array is being populated but I have tried to stop and restart the animation when the blocks array changes with useEffect but to no avail.
Here is a codesandbox of the issue Example Of Issue
In the sandbox you can see in the animate() function in the ScrollContainer component I am console logging the blocks array and then after the function I am logging the same array. When you scroll the array does not log the available blocks only an empty array. But the available blocks are being logged correctly under this function.
const animate = () => {
const diff = yScroll - yCurrent;
const delta = Math.abs(diff) < 0.1 ? 0 : diff * ease;
if (delta) {
yCurrent += delta;
yCurrent = parseFloat(yCurrent.toFixed(2));
animationFrame = requestAnimationFrame(animate);
} else {
cancelAnimation();
}
console.log("Animating Blocks", blocks);
};
console.log("Available Blocks", blocks);
const addBlock = block => {
setBlocks(prev => {
return [...prev, block];
});
};
and here is how I am starting the animation
const startAnimation = () => {
if (!animationFrame) {
animationFrame = requestAnimationFrame(animate);
}
};
useEffect(() => startAnimation(), []);
Thanks.
I played with your example. It seems to me, that the problem is in the useEffect. If you add empty dependency to it, then it runs only once after the first render. There will be a second render when blocks state updates, but for the useEffect only the first state is visible because it runs only once and it uses the startAnimation with stale closure. This version of startAnimation uses the first version of the animation with the original state.
Your initial problem is solved if you add blocks to the useEffect as a dependency.
useEffect(() => {
yScroll = window.scrollY || window.pageYOffset;
yCurrent = yScroll;
startAnimation();
window.addEventListener("scroll", updateScroll);
return () => {
window.removeEventListener("scroll", updateScroll);
};
}, [blocks]);
I tried adding the animation, but is is quite choppy to me. I'm interested in your final solution. This is mime: https://codesandbox.io/s/scrolling-animation-frame-array-issue-d05wz
I use higher level animation libraries like react-spring. You can consider using something like this. I think it is much easier to use.
https://codesandbox.io/s/staging-water-sykyj

State initialization inside a functional component (without infinite looping)

I have a functional component that holds custom viewport values in its state, so it must use event listeners and measure the window size:
const AppWrap = () => {
// custom vw and vh vars
const [vw, setvw] = useState();
const [vh, setvh] = useState();
// gets the inner height/width to act as viewport dimensions (cross-platform benefits)
const setViewportVars = () => {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// can be accessed in scss as vw(n), vh(n) OR in css as --vw * n, --vh * n
document.documentElement.style.setProperty('--vw', `${viewportWidth / 100}px`);
document.documentElement.style.setProperty('--vh', `${viewportHeight / 100}px`);
// can be accessed in child components as vw * n or vh * n
setvw(viewportWidth / 100);
setvh(viewportHeight / 100);
}
// I'd like to run this function *once* when the component is initialized
setViewportVars();
// add listeners
window.addEventListener('resize', setViewportVars);
window.addEventListener('orientationchange', setViewportVars);
window.addEventListener('fullscreenchange', setViewportVars);
return (
<App vw={vw} vh={vh}/>
);
}
The above code produces an error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
I can wrap setViewportVars() in useEffect, but I don't see why this is necessary. My understanding of functional components is that they only run code outside of the return statement once, and that only the JSX would re-render on a state change.
You have to use useEffect and pass empty array as dependencies, so this will only be excecuted once just like componentDidMount:
useEffect(() => {
setViewportVars();
// add listeners
window.addEventListener('resize', setViewportVars);
window.addEventListener('orientationchange', setViewportVars);
window.addEventListener('fullscreenchange', setViewportVars);
}, []);
So in your case what happens is basically you call the function it will update the state, so again component will load again function will call so basically that goes to infinite loop
Solution
you can useEffect, so in useEffect if you pass the second argument which is an array as empty it will called only one time like the componentDidMount
useEffect(() => {
setViewportVars()
}, [])
So if you pass second argument
Passing nothing, like useEffect(() => {}) - it will call every time.
Passing an empty array useEffect(() => {}, []) - it will call one time.
Passing array deps, whenever the array dependencies changes it will execute the code block inside the usEffect.
useEffect(() => {
// some logic
}, [user])
The answer to why you need to useEffect() to prevent the infinite re-render:
<AppWrap> has state {vw} and {vh}. When <AppWrap>is fired, setViewportVars() immediately runs and updates that state. Because you updated the state, setViewportVars() is then fired again (to keep in line with the react one way data flow which updates the state of {vw/vh} and causes a re-firing of AppWrap ...which causes a re-firing of setViewportVars(). At no point here have we allowed the DOM to get painted by the browser, we are just repeating the loop of:
init component > getHeight/Width > updateState > re-render component > getHeight/Width > ...
useEffect behaves differently than a regular render. useEffect fires only after a the DOM has been painted by the browser. Which means that the first cycle would finish (init component > browser paints DOM > useEffect(getHeight/Width) > |if state aka viewsize changed?| > re-render)
For more info, check out Dan Abramov's blog on useEffect
const AppWrap = () => {
// custom vw and vh vars
const [vw, setvw] = useState();
const [vh, setvh] = useState();
// gets the inner height/width to act as viewport dimensions (cross-platform benefits)
const setViewportVars = useCallback(() => {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// can be accessed in scss as vw(n), vh(n) OR in css as --vw * n, --vh * n
document.documentElement.style.setProperty('--vw', `${viewportWidth / 100}px`);
document.documentElement.style.setProperty('--vh', `${viewportHeight / 100}px`);
// can be accessed in child components as vw * n or vh * n
setvw(viewportWidth / 100);
setvh(viewportHeight / 100);
}, []);
useEffect(() => {
window.addEventListener('resize', setViewportVars);
window.addEventListener('orientationchange', setViewportVars);
window.addEventListener('fullscreenchange', setViewportVars);
return () => {
window.removeEventListener('resize', setViewportVars);
window.removeEventListener('orientationchange', setViewportVars);
window.removeEventListener('fullscreenchange', setViewportVars);
}
}, []);
useEffect(() => {
// I'd like to run this function *once* when the component is initialized
setViewportVars();
}, []);
return (
<App vw={vw} vh={vh} />
);
}

how can I choose to send pass different props to a react component according to the current viewport size?

Is there a way to pass different props to a component according to the current viewport size?
I know you can get the current height and width using
var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
Image between 1440px and 1000px I have send some data to a component, and when it viewport shrinks below 1000px and above 768px I need to send another set of data to that component. How can I achieve this functionality in React?
There are several ways to handle this. I would recommend to make use of the ResizeObserver API:
https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
There are polyfills for this if needed:
https://github.com/juggle/resize-observer
You can set the props you want to send in your component state, and then listen for a resize event. When the window resizes you get the new window size and set the props you want to pass to the component in the state accordingly. A rerender is triggered by React and the props, belonging to your window size, are passed to the child component.
You could create listener for resize event and provide decide if it is mobile or not or anything else based upon the width (isMobile being an example).
Code below using this approach
But this approach would affect performance because of unnecessary renders. Then you would want to add debouncing or throttling for performance improvements.
So to avoid this I would recomended using react-sizes
class MyComponent extends React.Component {
state = {
isMobile: false
}
handleWindowResize = () => {
this.setState({ isMobile: window.innerWidth < 480 });
}
componentDidMount() {
window.addEventListener('resize', this.onWindowResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.onWindowResize);
}
render() {
const { items } = this.props;
const { isMobile } = this.state;
return (
<SomeComponent isMobile={isMobile}>
{items}
</SomeComponent>
);
}
}
Reference of code
Hope it helps!!!

Can not get height value using underscore throttle Vue.js

i just want to get height value every second i put in second parameter in _.throttle() using vue.js but doesn't work :(
Here my vue code:
import underscore from 'underscore';
export default {
data() {
return {
progress: 0,
height: 0,
scroll: 0
}
},
created() {
window.addEventListener('scroll', _.throttle(this.handleScroll, 300));
},
methods: {
/**
* Handle the scroll.
*/
handleScroll() {
this.height = _.throttle(document.getElementById('tags-module').offsetTop, 300);
this.scroll = window.scrollY;
this.progress = Math.floor((this.scroll/this.height)*100);
}
}
}
There in handleScroll() method, this.height i used throttle but in can't get the height value. and if i remove the throttle, works fine.
Please tell me where's my mistake.
Try this.
methods:{
handleScroll: _.throttle(function(){
this.scroll = window.scrollY;
this.progress = Math.floor((this.scroll/this.height)*100);
}, 300),
updateHeight: _.throttle(function(){
this.height = document.getElementById('tags-module').offsetTop;
}, 1000)
},
created(){
window.addEventListener('scroll', () => {
this.updateHeight();
this.handleScroll();
});
}
Given that the handleScroll method is already throttled, you don't need a second one. However, if you wish to keep it, you need to return the value:
this.height = _.throttle(
() => document.getElementById('tags-module').offsetTop,
300
)()
From the manual:
Creates and returns a new, throttled version of the passed function, that, when invoked repeatedly, will only actually call the original function at most once per every wait milliseconds. Useful for rate-limiting events that occur faster than you can keep up with.

Store variables between ReactJs component method calls

togglePreloader: function(toggleState) {
var timeout = null;
var time = 0;
if (toggleState) {
if (this.state.preloading) console.log("Preloader alredy shown");
if (timeout) {
clearTimeout(timeout);
timeout = null;
} else if (!this.state.preloading) {
console.log("Show preloader");
time = Date.now();
this.setState({ preloading: true });
}
} else {
if (!timeout) {
var elapsed = Date.now() - time;
if (elapsed < Config.PRELOADER_MIN_DISPLAY_DURATION) {
console.log("Preloader hiding timeout was started; elapsed: " + elapsed + "/" + Config.PRELOADER_MIN_DISPLAY_DURATION);
timeout = setTimeout(function() {
timeout = null;
this.setState({ preloading: false });
console.log("Hide preloader by timeout");
}.bind(this), Config.PRELOADER_MIN_DISPLAY_DURATION - elapsed);
} else {
this.setState({ preloading: false });
console.log("Hide preloader; elapsed: " + elapsed + "/" + Config.PRELOADER_MIN_DISPLAY_DURATION);
}
} else console.log("Preloader hiding is waiting for timeout");
}
}
This is the method of reactJs component. It trigger to show and hide preloader. If preloader was displayed less than minimal duration (e.g 500ms) it sets timeout on hiding.
The question is where to store variables timeout and time between calls of togglePreloader. Mutating this.props is not a good idea. Changes in this.state triggers rerendering. Moving variables out of component ? Or using state with shouldComponentUpdate ? What is the best way ? Im new to reactJs
It's not just not a good idea, you don't get to play with this.props, it's the collection of data that the component's parent controls. You can use state, which will render, or you can just do the obvious thing: just use this.timeout = ..., since your React component is still just a JavaScript object with its own instance scoping.
this.props.xyz for values that you were assigned "from above"
this.state.xyz for values that you control and directly influence what the UI should look like
this.xyz for any transient values that have no influence on the UI and can technically be reset without any adverse effects.
However, consider that the timeout value is universal, so should probably be a static:
var YourComponent = React.createClass({
statics: {
timeout: 500
},
...
checkStuff: function() {
if (this.currentTime >= YourComponent.timeout) {
this.doAThing();
}
},
...
});
And if the idea is that different things happen to the UI based on that timeout, then really you should trigger some state value that your component can then use in render() to make sure it's now showing, or doing, the right thing. So you use a this.currentTime to track timing so far, and then a state variable once you know you've passed the threshold.
If you're new to react, it's 100% worth running through the "Getting started" and "tutorial" sections on the React website. Just sit down on them, do all of it in one go -it takes less than 30minutes- and you'll have a much better idea of how you're supposed to work with React. And then if you still need more insight, there are a lot of great articles on React on the googles.

Categories

Resources