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
Related
I am working on my portfolio, but when I reach the experiences page I tried to create a title that has it's middle part change every second, and value of it must come from an already set up array,
but when I run the code it always return the first string of the array,
can anyone please fix this problem for me ?
const projectsTitleKeyWords = ['responsible', 'meaningful', 'beautiful']
let titlep2 = 'test'
let index = 0
const change = () => {
titlep2 = projectsTitleKeyWords[index]
index = ++index % projectsTitleKeyWords.length
setTimeout(change, 1000)
}
change()
console.log(titlep2)
const titlep1 = 'I creat '
const titlep1Array = titlep1.split('')
let titlep2Array = titlep2.split('')
const titlep3 = ' projects'
const titlep3Array = titlep3.split('')
the value of titlep2Array will be received by
<AnimatedLetters
letterClass={letterClass}
strArray={titlep2Array}
idx={15}
id='to-change'
/>
In-order to reflect UI changes in React, a component must re-render.
A React component re-renders in 1 of 2 scenarios:
Whenever there's a change in the value of a local state.
Whenever any of it's parent components re-render.
Therefor, since changes in the UI are only reflected upon a re-render, we should manage a local state that would be responsible for this behavior.
With functional components in React, this can be achieved via the useState hook.
In your case, we can simply make titlep2 a state, instead of a regular variable.
Example:
const [titlep2, setTitlep2] = useState('')
const change = () => {
setTitlep2(projectsTitleKeyWords[index])
index = ++index % projectsTitleKeyWords.length
setTimeout(change, 1000)
}
<AnimatedLetters
letterClass={letterClass}
strArray={titlep2.split('')}
idx={15}
id='to-change'
/>
Note: since this function now updates the state, we can't call it the way you did in your example, since it will run every time the component re-renders, making the component re-render indefinitely due to the change in state.
Therefor, we can use the useEffect hook in-order to allow it to run only once on the initial render.
Example:
const change = () => {
setTitlep2(projectsTitleKeyWords[index])
index = ++index % projectsTitleKeyWords.length
setTimeout(change, 1000)
}
useEffect(() => {
change()
}, [])
Furthermore, if there are any other variables that should reflect changes in the UI, they can be convert to states as well.
for that try using the setInterval() instead of setTimeout().
You are trying to make the text change after a specific interval in this case 1 second.
You should also consider doing that will CSS animations, it seems this is overkill.
const change = () => {
titlep2 = projectsTitleKeyWords[index]
index = ++index % projectsTitleKeyWords.length
console.log(titlep2)
setInterval(change, 1000)
}
change()
I created a drag and drop menu and so far everything works, however, I have run into a small coding problem for my last step. I need to get the menu of items to automatically scroll when the dragged item is towards the bottom of the container (or top). I decided to structure it so that when the cursor dragging the item falls below a specific range towards the bottom, a scroll function will trigger and continually scroll down at a pace I can set. In order to achieve this I set up a setInterval function and will continually call a scrollBy function.
My issue is that In order to stop the setInterval and call clearInterval, I need to know when the position of the cursor is no longer in that range. Every time the function is called in setInterval, it takes a snapshot of the current state variables. Instead of updating the state of those variables (yAxis position) within the function, it is stuck on the first value it gets.
The yPoisiton is obtained with the window.event.clientY from another component.
document.onmousemove = (e) => {
if (dragging.state) {
var newYPos = window.event.clientY;
setYPos(newYPos);
handleScroll(newYPos, dragging.state);
The rest of the code goes as follows:
const menuContainer = useRef();
const [scrolling, setScrolling] = useState(false);
const [yPos, setYPos] = useState(null);
const [dragging, setDragging] = useState({
state: false,
index: null,
y: null,
height: null,
});
function continueScrolling() {
console.log("continueScrolling function");
console.log(yPos); // Same value even after updating via setYPos)
const date = new Date();
console.log(date.toLocaleTimeString()); //Different values in accordance to the time
}
console.log("Index.js yPos");
console.log(yPos); // Different values every render when dragging
const handleScroll = (draggingYPos, isDragging) => { // This is triggered onMouseMove in another component
if (
draggingYPos >
menuContainer.current.getBoundingClientRect().bottom - 100 &&
isDragging
) {
setScrolling(true); // On the first cycle scrolling == false and executes the setInterval function. Each subsequent call is then skipped.
if (scrolling) {
return;
} else {
var intr = setInterval(function () {
continueScrolling(); // Different method of testing and trying to get the current yPos in the function
//Commented but shows a bit of what I'm trying to accomplish
// if (
// !(
// yPos >
// menuContainer.current.getBoundingClientRect().bottom - 100 &&
// dragging.state
// )
// )
// clearInterval(intr);
}, 1000);
Please let me know if there is more information you need or anything else I can clarify.
This is a temporary solution I found to my problem, however, I would still like to know a more proper and robust solution if anyone has one, as well as information as to the scope of functions, React state, and how to pass them between one another.
I realize that it is not recommended to mutate states directly, however, this is my only current working solution. If I create a temporary value and pass in the setInterval, it will call setInterval a second time and will still be active after clearing it.
If I declare a global const variable, I cannot reassign the variable with setInterval. When I declared a global variable with var, the setInterval function continued even after I cleared it.
Adjusted code:
const [scroll, setScroll] = useState(null);
const handleScroll = (draggingYPos, isDragging) => {
if (
draggingYPos >
menuContainer.current.getBoundingClientRect().bottom - 100 &&
isDragging
) {
setScrolling(true);
if (scrolling) {
return;
} else {
var count = 0;
if (
!(
yPos > menuContainer.current.getBoundingClientRect().bottom - 100 &&
dragging.state
)
) {
setScroll(
setInterval(function () {
count += 1;
console.log(count);
}, 1000)
);
}
}
} else {
console.log("Not Scrolling");
setScroll(clearInterval(scroll));
setScrolling(false);
}
};
I have a basic rotating cube I've made with react-three-fiber, and onPointerOver (also tried onPointerEnter) I want to smoothly rotate it to its starting position via useSpring. However, not only does it not do so when I hover, but it will only do so onClick. I have a totally different function that executes onClick -- and if I disabled that prop then the rotation reset fails altogether.
I've got a pretty simple set up in my Box component:
export const Box = () => {
const [active, setActive] = useState(false);
const boxRef = useRef<Mesh>();
const starterRotation = [0, 1, 1];
useFrame(() => {
if (boxRef.current) {
boxRef.current.rotation.x += 0.01;
boxRef.current.rotation.y -= 0.01;
}
});
const [resetRotation, setSpring] = useSpring(() => ({
rotation: starterRotation
})); // trying to use the `set` syntax to call the useSpring and smoothly animate to a certain rotation.
const springs = useSpring({ scale: active ? 1.5 : 1, config: config.wobbly });
return (
<animated.mesh
// #ts-ignore
rotation={resetRotation.rotation}
scale={springs.scale}
onClick={() => setActive(!active)}
onPointerOver={() => setSpring.start()}
ref={boxRef}
>
<boxGeometry args={[2, 1, 2]} />
<meshStandardMaterial color="royalblue" />
</animated.mesh>
);
};
Here's a live example: https://codesandbox.io/s/react-spring-rotating-cube-np4v6
From what I can tell in their docs this is the correct way. I also saw some github issues discussions on it, and there seemed to be some concern about the second argument of the useSpring deconstruction not working properly, but the rotation works for me -- it's jut the triggering event that won't work.
It does work, the issue is you're still updating the rotation based on the ref in a useFrame call so that's updated every frame. Which will override the animation value.
The second issue is that if you stoped the useFrame from animating rotation it won't work because the spring's internal value will be set to [0,1,1] but that's what you want it to animate to.
This is a good opportunity to use from and to props of useSpring, what I would do is use a ref to stop the useFrame animation and then use the ref to get the current values of the rotation to use as from in the springSet.start function and then to which is the starterRotation value you've declared.
You can see this in effect here – https://codesandbox.io/s/react-spring-rotating-cube-forked-1j02g?file=/src/components/Box.tsx
I am trying to create an easing function on scroll but my main function is growing rather large and I want to be able to split it up. I am creating a requestAnimationFrame function that will ease the page scroll. The big issue I am having is that the render function with the animation frame will ease the Y value and then calls the update function to update the elements. But if I split this up and import them individually I am having trouble figuring out how to pass the updated values between functions. Many other functions also rely on these updated values.
I could take an object oriented approach and create a class or a constructor function and bind this to the function but it seems like bad practice to me:
import render from './render'
import update from './update'
const controller = () => {
this.items = [];
this.event = {
delta: 0,
y: 0
}
this.render = render.bind(this)
this.update = update.bind(this)
//ect
}
I could also break it up into classes that extend each other but I would like to take a more functional approach to the situation but I am having trouble figuring out how to achieve this.
Here is a very condensed version of what I am trying to do. Codesandbox.
const controller = (container) => {
const items = [];
let aF = null;
const event = {
delta: 0,
y: 0
};
const render = () => {
const diff = event.delta - event.y;
if (Math.abs(diff) > 0.1) {
event.y = lerp(event.y, event.delta, 0.06);
aF = requestAnimationFrame(render);
} else {
stop();
}
update();
};
const start = () => {
if (!aF) aF = requestAnimationFrame(render);
};
const stop = () => {
event.y = event.delta;
cancelAnimationFrame(aF);
aF = null;
};
const update = () => {
const y = event.y;
container.style.transform = `translateY(-${y}px)`;
items.forEach((item) => {
item.style.transform = `translate(${y}px, ${y}px)`;
});
};
const addItem = (item) => items.push(item);
const removeItem = (item) => {
const idx = items.indexOf(item);
if (idx > -1) items.splice(idx, 1);
};
const onScroll = () => {
event.delta = window.scrollY;
start();
};
// and a bunch more stuff
window.addEventListener("scroll", onScroll);
return {
addItem,
removeItem
};
};
export default controller;
I would like to be able to split this up and create a more functional approach with pure functions where I can import the update and render functions. The problem is that these functions are reliant on the updated global variables. Everywhere I look it says that global variables are a sin but I don't see a way to avoid them here. I have tried to look at the source code for some large frameworks to get some insight on how they structure their projects but it is too much for me to take in at the moment.
Any Help would be appreciated. Thanks.
What you wrote in the first example is completely valid, you basically did what classes are doing under the hood (classes are just syntactic sugar almost), but there are some problems with your code:
You use an arrow function, so this becomes window actually in that context.
You should use function expression instead, and initialize your instance using new, only in this case this will work how you want.
However, there are many solutions for Dependency Injection / Plugin systems which is what you're basically looking for, I'd advise you to look around this area.
In case you want a more functional approach, you need to avoid the this keyword and you need to use pure functions. I'd advice you not to share values in a context, but simply pass the necessary dependencies to your functions.
import render from './render'
const controller = () => {
const items = []
window.addEventListener('scroll', (event) => {
start({ y: event.y })
})
const start = ({ y }) => {
// Pass what render needs
requestAnimationFrame(() => render({ container, items, y }))
}
}
Using the above:
You don't need the update function in this file, render will import it on its own.
Your functions are pure, easily testable on their own.
You don't need to create an instance by using new (it's actually more expensive).
No need for shared state across functions.
No DI/Plugin solution need to implemented/used.
In case you do want to have shared state, you won't do that without classes and/or extra tooling around.
The way I'd do this is by creating a seperate .js file, then send the information you need into that js file right at the start of your main files code. Then just use the seperate file to store all your functions, then just call them with utils.myFunction(). At least that's how I do it in node.
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()
}