Store variables between ReactJs component method calls - javascript

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.

Related

React: Tell child component to "reinitialize," even when the passed props are the same

I have a MyComponent that renders a Timer component. My current setup is like this:
MyComponent.render:
render () {
return <Timer time={this.state.time} lag={this.lag || 0} />
}
Timer:
class Timer extends Component {
constructor(props) {
super(props);
this.state = {
time: this.props.time,
};
}
startTimer = (duration) => {
if (duration > 0){
this.on = true;
let timer = duration * 1000 + this.props.lag;
var s = setInterval(() => {
this.setState({time: Math.round(timer/1000)});
timer = timer - 500;
if (timer <= 0) {
this.on = false;
clearInterval(s);
}
}, 500);
}
}
componentDidMount = () => {
this.startTimer(this.props.time);
}
render() {
return (
<div className="Timer-container">
<div className="Timer-value">{this.state.time}</div>
</div>
);
}
}
As you can see, when the Timer is initialized, it immediately starts counting down. On subsequent renders of MyComponent, I want to restart the Timer, even if the time prop doesn't change. In other words, I want it to "reinitialize" on every render. How do I achieve this?
First of all, to reset the counter, you need to store something in the state,
either the interval (so you can clear it)
or the current time (so you can set it to the initial value).
As you want to do something if the parent re-rendered (but the props didn't change), basically what you need to check is why your component updated. An answer to that would be "Trace why a React component is re-rendering"
A quick way for your example would be to check if the state has changed (not recommended):
componentDidUpdate(prevProps, prevState, snapshot) {
if( prevState === this.state ){
clearInterval( this.state.interval );
this.startTimer( this.props.time );
}
}
Another quick solution would be (if it is an option for you) to pass a shouldRerender property to the component, and then check for this property inside the component:
// -- inside MyComponent
render () {
return <Timer
time={ this.state.time }
lag={ this.lag || 0 }
shouldRerender={ {/* just an empty object */} } />;
}
// -- inside Timer
componentDidUpdate(prevProps, prevState, snapshot) {
if( prevProps.shouldRerender !== this.props.shouldRerender ){
clearInterval( this.state.interval );
this.startTimer( this.props.time );
}
}
That looks a bit "dirty" to me. A cleaner way would be to pass some state to shouldRerender, which changes on every update (e.g. just an increasing number).
However, I think the approach to check if parent rendered is not the React way. I, personally, do consider if a component renders or not an implementation detail (I don't know if that's correct to say), that is, I don't care when React decides to render, I only care for props and state (basically).
I would recommend to think about what actually is "cause and effect", what is the reason why you want to reset the timer. Probably the re-render of the parent is only the effect of some other cause, which you might be able to use for your time reset, too.
Here some different concepts that might be useful for use cases I can imagine:
not use one Time instance, but destroy and create inside parent when needed, maybe also using a key prop.
use a HOC (like withTimer) or custom hook (like useTimer), injecting a reset() function (plus create a separate TimerView component)
keep the time state in MyComponent, passing time and onChange down to the Timer component (<Timer time={ this.state.time } onChange={ time => { this.setState({ time: time }); } } />), then both MyComponent and Timer can set / reset the time.

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

this.setState does not seem to be called on react native iOS

I am currently using setState in my react-native app to render a screen.
Below is the code, for some reason the code runs everything normally except
setting the state.
showCard(event) {
const { loadCard } = this.state;
this.setState({ loadCard: true });
console.log(loadCard)
// get value of marker ID
const markerID = parseInt(event.nativeEvent.id);
console.log(markerID)
if (markerID <= this.state.markers.length && markerID != null) {
setTimeout(() => {
//USE THIS FORMAT
this.scrollView.getNode().scrollTo({
x: markerID * (CARD_WIDTH),
animated: true
});
}, 0.01);
console.log(this.scrollView)
}
}
Advice on this matter will be greatly appreciated, as it works on android for me
It does work but setting state is asynchronous so you can't just log the result after, observe with the following change:
this.setState({ loadCard: true }, () => console.log(this.state.loadCard));
The second argument is a callback fired after state is set, but you very rarely need that in reality.
Also setTimeout delay is an integer representing milliseconds not a floating point. If you want it as soon as possible use 0.
You need to bind your functions in the constructor while settling the state
constructor(props){
super(props);
this.state = {
loadCard : false;
}
this.changeCard = this.changeCard.bind(this);
}
changeCard = (userId) => {
this.setState({ loadCard: true});
}
render(){
return(
console('working state'+ this.state.loadCard);
);
}
I have somehow solved the issue. The main cause apparently as I was working with react-native maps dependency, it could not register the input directly from the marker component and required the method to be called in the main map.
But thank you for all the input guys!

Vue JS - How to get window size whenever it changes

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()
}

Clear Interval doesn't work in VUE

I am trying to build a Pomodoro Timer using VUE.js, below is the relevant code snippet that I had problems on. So the problem is that the clearInterval Method doesn't seem to work here.
data: {
workTimeInMin: 1,
breakTimeInMin: 1,
timeRemainInMillisecond: 0,
timerOn: false,
rest: false
},
methods: {
startOrStopTheTimer: function() {
var self = this;
var interval;
if (this.timerOn){
this.timerOn = !this.timerOn;
window.clearInterval(interval);
console.log("clearInterval");
}else {
this.timerOn = !this.timerOn;
interval = setInterval(function(){
console.log("123");
if (self.timeRemainInMillisecond <= 0) {
console.log("1");
self.rest = !self.rest;
if (self.rest) {
self.timeRemainInMillisecond = self.restTimeInMin*60*1000;
}else {
self.timeRemainInMillisecond = self.workTimeInMin*60*1000;
}
}
this.timeRemainInMillisecond = this.timeRemainInMillisecond-1000
}, 1000);
}
},
You can find a live demo here.
In the page, when I click start an set Interval method is called. Then I clicked the stop, it is supposed to clear the interval, however it doesn't seem to work as I intend it to. You can inspect the console and easily find the "123" keeps popping up which indicates that the interval is not cleared.
After searching a while in the StackOverFlow, I found that if I define the variable interval to in the global context it will work as I intend it. But I wish to know why it is so.
Your help will be highly appreciated. Thanks in advance!
Your problem is that var interval is a declared new every time the function is called. So the variable that holds a reference to your interval is not accessible.
Just use this.interval and the variable will added to the data-section of your component under the hood.
You can also just use this mixin (plugin) / have a look how it works there:
Vue interval mixin
You can clear setInterval and clearinterval. here is sample code please modify according to your need
if (val && !this.t) {
this.t = setInterval(() => {
location.reload()
}, 10 * 1000)
} else {
clearInterval(this.t)
}
I arrived at this page because Vue still seems to handle setInterval in the strange way as described by the OP. I tried all the fixes above and found only two options work:
Global scope the interval variable (as mentioned above and less than an ideal workaround)
Use Vuex to store the timer and handle it the usual Vuex way.
I was happy to find that using Vuex bypasses the problem, since I'm using Vuex anyway. It doesn't shed light on why this issue exists, but it does give us a way to move forward with little sacrifices made.
So just declare the variable in the Vuex state, use mutations to start and stop the timer and all works as expected.
This works like a charm:
data () {
return {
polling: null
}
},
methods: {
pollData () {
this.polling = setInterval(() => {
console.log('fired...')
}, 3000)
}
},
beforeDestroy () {
clearInterval(this.polling)
},
created () {
this.pollData()
}

Categories

Resources