Problem
I'm converting a classful component into a functional component and state re-rendering has some issues.
Here is the original classful component:
class Skills extends React.Component {
constructor(props) {
super(props);
const skills = [
"HTML",
"CSS",
"SCSS",
"Python",
"JavaScript",
"TypeScript",
"Dart",
"C++",
"ReactJS",
"Angular",
"VueJS",
"Flutter",
"npm",
"git",
"pip",
"Github",
"Firebase",
"Google Cloud",
];
this.state = {
skills: skills.sort(() => 0.5 - Math.random()),
isLoaded: false,
points: new Array(skills.length).fill([[0], [0], [-200]]),
sphereLimit: 1,
xRatio: Math.random() / 2,
yRatio: Math.random() / 2,
isMounted: true,
};
}
fibSphere(samples = this.state.skills.length) {
// https://stackoverflow.com/a/26127012/10472451
const points = [];
const phi = pi * (3 - sqrt(5));
for (let i = 0; i < samples; i++) {
const y = (i * 2) / samples - 1;
const radius = sqrt(1 - y * y);
const theta = phi * i;
const x = cos(theta) * radius;
const z = sin(theta) * radius;
const itemLimit = this.state.sphereLimit * 0.75;
points.push([[x * itemLimit], [y * itemLimit], [z * itemLimit]]);
}
this.setState({
points: points,
isLoaded: true,
});
}
rotateSphere(samples = this.state.skills.length) {
const newPoints = [];
const thetaX = unit(-this.state.yRatio * 10, "deg");
const thetaY = unit(this.state.xRatio * 10, "deg");
const thetaZ = unit(0, "deg");
const rotationMatrix = multiply(
matrix([
[1, 0, 0],
[0, cos(thetaX), -sin(thetaX)],
[0, sin(thetaX), cos(thetaX)],
]),
matrix([
[cos(thetaY), 0, sin(thetaY)],
[0, 1, 0],
[-sin(thetaY), 0, cos(thetaY)],
]),
matrix([
[cos(thetaZ), -sin(thetaZ), 0],
[sin(thetaZ), cos(thetaZ), 0],
[0, 0, 1],
])
);
for (let i = 0; i < samples; i++) {
const currentPoint = this.state.points[i];
const newPoint = multiply(rotationMatrix, currentPoint)._data;
newPoints.push(newPoint);
}
if (this.state.isMounted) {
this.setState({ points: newPoints });
setTimeout(() => {
this.rotateSphere();
}, 100);
}
}
handleMouseMove(e) {
let xPosition = e.clientX;
let yPosition = e.clientY;
if (e.type === "touchmove") {
xPosition = e.touches[0].pageX;
yPosition = e.touches[0].pageY;
}
const spherePosition = document
.getElementById("sphere")
.getBoundingClientRect();
const xDistance = xPosition - spherePosition.width / 2 - spherePosition.x;
const yDistance = yPosition - spherePosition.height / 2 - spherePosition.y;
const xRatio = xDistance / this.state.sphereLimit;
const yRatio = yDistance / this.state.sphereLimit;
this.setState({
xRatio: xRatio,
yRatio: yRatio,
});
}
updateWindowDimensions() {
try {
const sphere = document.getElementById("sphere");
if (
this.state.sphereLimit !==
Math.min(sphere.clientHeight, sphere.clientWidth) / 2
) {
this.setState({
sphereLimit: Math.min(sphere.clientHeight, sphere.clientWidth) / 2,
});
this.fibSphere();
}
} catch (error) {
console.error(error);
}
}
componentDidMount() {
document.title =
window.location.pathname === "/skills"
? "Josh Pollard | ⚙️"
: document.title;
setTimeout(() => {
this.fibSphere();
this.updateWindowDimensions();
this.rotateSphere();
}, 1500);
window.addEventListener("resize", () => this.updateWindowDimensions());
}
componentWillUnmount() {
this.setState({ isMounted: false });
window.removeEventListener("resize", () => this.updateWindowDimensions());
}
render() {
return (
<motion.div
className="skills-body"
initial="initial"
animate="animate"
exit="exit"
custom={window}
variants={pageVariants}
transition={pageTransition}
onMouseMove={(e) => this.handleMouseMove(e)}
onTouchMove={(e) => this.handleMouseMove(e)}
>
<div className="skills-info-container">
<div className="skills-title">Skills</div>
<div className="skills-description">
I am a driven and passionate aspiring software engineer. I have
invested a significant amount of time and effort in self-teaching,
developing my knowledge and supporting others in the field of
digital technology. I thrive on the challenge of finding intelligent
solutions to complex problems and I am keen to apply and grow my
skills in the workplace.
</div>
</div>
<div className="sphere-container" id="sphere">
{this.state.isLoaded &&
this.state.skills.map((skill, index) => (
<motion.div
className="sphere-item"
key={index}
initial={{ opacity: 0 }}
animate={{
x: this.state.points[index][0][0],
y: this.state.points[index][1][0] - 20,
z: this.state.points[index][2][0],
opacity: Math.max(
(this.state.points[index][2][0] / this.state.sphereLimit +
1) /
2,
0.1
),
}}
transition={{
duration: 0.1,
ease: "linear",
}}
>
{skill}
</motion.div>
))}
</div>
</motion.div>
);
}
}
It's essentially a sphere of words that moves depending on mouse movement demo
Now this is as far as I have gotten with the migration to a Functional component:
function Skills(props) {
const skills = [
"HTML",
"CSS",
"SCSS",
"Python",
"JavaScript",
"TypeScript",
"Dart",
"C++",
"ReactJS",
"Angular",
"VueJS",
"Flutter",
"npm",
"git",
"pip",
"Github",
"Firebase",
"Google Cloud",
].sort(() => 0.5 - Math.random());
const [points, setPoints] = useState(
new Array(skills.length).fill([0, 0, -200])
);
const [sphereLimit, setSphereLimit] = useState(1);
const [xRatio, setXRatio] = useState(Math.random() / 2);
const [yRatio, setYRatio] = useState(Math.random() / 2);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
document.title =
window.location.pathname === "/skills"
? "Josh Pollard | ⚙️"
: document.title;
let interval;
setTimeout(() => {
updateWindowDimensions();
interval = setInterval(rotateSphere, 100);
}, 1500);
window.addEventListener("resize", updateWindowDimensions);
return () => {
clearInterval(interval);
window.removeEventListener("resize", updateWindowDimensions);
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const fibSphere = (samples = skills.length) => {
// https://stackoverflow.com/a/26127012/10472451
const newPoints = [];
const phi = pi * (3 - sqrt(5));
for (let i = 0; i < samples; i++) {
const y = (i * 2) / samples - 1;
const radius = sqrt(1 - y * y);
const theta = phi * i;
const x = cos(theta) * radius;
const z = sin(theta) * radius;
const itemLimit = sphereLimit * 0.75;
newPoints.push([x * itemLimit, y * itemLimit, z * itemLimit]);
}
console.log(newPoints);
setPoints(newPoints);
setIsLoaded(true);
};
const rotateSphere = (samples = skills.length) => {
const newPoints = [];
const thetaX = unit(-yRatio * 10, "deg");
const thetaY = unit(xRatio * 10, "deg");
const thetaZ = unit(0, "deg");
const rotationMatrix = multiply(
matrix([
[1, 0, 0],
[0, cos(thetaX), -sin(thetaX)],
[0, sin(thetaX), cos(thetaX)],
]),
matrix([
[cos(thetaY), 0, sin(thetaY)],
[0, 1, 0],
[-sin(thetaY), 0, cos(thetaY)],
]),
matrix([
[cos(thetaZ), -sin(thetaZ), 0],
[sin(thetaZ), cos(thetaZ), 0],
[0, 0, 1],
])
);
for (let i = 0; i < samples; i++) {
const currentPoint = points[i];
const newPoint = multiply(rotationMatrix, currentPoint)._data;
newPoints.push(newPoint);
}
console.log(newPoints[0]);
console.log(points[0]);
setPoints(newPoints);
};
const handleMouseMove = (e) => {
let xPosition = e.clientX;
let yPosition = e.clientY;
if (e.type === "touchmove") {
xPosition = e.touches[0].pageX;
yPosition = e.touches[0].pageY;
}
const spherePosition = document
.getElementById("sphere")
.getBoundingClientRect();
const xDistance = xPosition - spherePosition.width / 2 - spherePosition.x;
const yDistance = yPosition - spherePosition.height / 2 - spherePosition.y;
const xRatio = xDistance / sphereLimit;
const yRatio = yDistance / sphereLimit;
setXRatio(xRatio);
setYRatio(yRatio);
};
const updateWindowDimensions = () => {
try {
const sphere = document.getElementById("sphere");
if (
sphereLimit !==
Math.min(sphere.clientHeight, sphere.clientWidth) / 2
) {
setSphereLimit(Math.min(sphere.clientHeight, sphere.clientWidth) / 2);
fibSphere();
}
} catch (error) {
console.error(error);
}
};
return (
<motion.div
className="skills-body"
initial="initial"
animate="animate"
exit="exit"
custom={window}
variants={pageVariants}
transition={pageTransition}
onMouseMove={handleMouseMove}
onTouchMove={handleMouseMove}
>
<div className="skills-info-container">
<div className="skills-title">Skills</div>
<div className="skills-description">
I am a driven and passionate aspiring software engineer. I have
invested a significant amount of time and effort in self-teaching,
developing my knowledge and supporting others in the field of digital
technology. I thrive on the challenge of finding intelligent solutions
to complex problems and I am keen to apply and grow my skills in the
workplace.
</div>
</div>
<div className="sphere-container" id="sphere">
{isLoaded &&
skills.map((skill, index) => (
<motion.div
className="sphere-item"
key={index}
initial={{ opacity: 0 }}
animate={{
x: points[index][0],
y: points[index][1] - 20,
z: points[index][2],
opacity: Math.max(
(points[index][2] / sphereLimit + 1) / 2,
0.1
),
}}
transition={{
duration: 0.1,
ease: "linear",
}}
>
{skill}
</motion.div>
))}
</div>
</motion.div>
);
}
Investigation
Now when I run this functional version it seems that for every state update the component is 'reset', instead of updating the UI, here is a codesandbox env
When highlighting one of the 'skill' words in the browser, it seems to be switching length very quickly (every 100ms, the same interval as the rotation sphere). This can be confirmed by going into dev tools and seeing that each 'skill' word changes every 100ms.
Unless I've got this wrong, this doesn't seem right at all. The skills variable in the functional component is a const
so shouldn't change on state changes?
I feel like I'm missing something very obvious, any help appreciated!
Here is a working piece (functional component) of your class component:
import React, { useEffect, useRef, useState } from 'react'
import { motion } from 'framer-motion'
import { matrix, multiply, sin, cos, sqrt, pi, unit } from 'mathjs'
import './skills.scss'
const pageTransition = {
ease: [0.94, 0.06, 0.88, 0.45],
duration: 1,
delay: 0.5,
}
const pageVariants = {
initial: (window) => ({
position: 'fixed',
clipPath: `circle(0px at ${window.innerWidth / 2}px ${
window.innerHeight / 2
}px)`,
}),
animate: (window) => ({
clipPath: `circle(${
Math.max(window.innerWidth, window.innerHeight) * 4
}px at ${window.innerWidth / 2}px ${window.innerHeight / 2}px)`,
position: 'absolute',
}),
exit: {
display: 'fixed',
},
}
const skills = [
'HTML',
'CSS',
'SCSS',
'Python',
'JavaScript',
'TypeScript',
'Dart',
'C++',
'ReactJS',
'Angular',
'VueJS',
'Flutter',
'npm',
'git',
'pip',
'Github',
'Firebase',
'Google Cloud',
]
export default function Skills() {
const sphere = useRef(undefined)
const [isLoaded, setIsLoaded] = useState(true)
const [points, setPoints] = useState(
new Array(skills.length).fill([[0], [0], [-200]]),
)
const [sphereLimit, setSphereLimit] = useState(1)
const [xRatio, setXRatio] = useState(Math.random() / 2)
const [yRatio, setYRatio] = useState(Math.random() / 2)
const [triggerBy, setTriggerBy] = useState('')
const [sphereItem, setSphereItem] = useState([])
useEffect(() => {
if (triggerBy === 'fibSphere') {
rotateSphere()
}
if (triggerBy === 'rotateSphere') {
setTimeout(() => {
rotateSphere()
}, 100)
}
})
useEffect(() => {
fibSphere()
updateWindowDimensions()
//rotateSphere();
}, [sphereItem])
const fibSphere = () => {
// https://stackoverflow.com/a/26127012/10472451
let newPoints = []
const phi = pi * (3 - sqrt(5))
for (let i = 0; i < skills.length; i++) {
const y = (i * 2) / skills.length - 1
const radius = sqrt(1 - y * y)
const theta = phi * i
const x = cos(theta) * radius
const z = sin(theta) * radius
const itemLimit = sphereLimit * 0.75
newPoints.push([[x * itemLimit], [y * itemLimit], [z * itemLimit]])
}
setPoints(newPoints)
setIsLoaded(true)
setTriggerBy('fibSphere')
}
const rotateSphere = () => {
let newPoints = []
const thetaX = unit(-1 * yRatio * 10, 'deg')
const thetaY = unit(xRatio * 10, 'deg')
const thetaZ = unit(0, 'deg')
const rotationMatrix = multiply(
matrix([
[1, 0, 0],
[0, cos(thetaX), -sin(thetaX)],
[0, sin(thetaX), cos(thetaX)],
]),
matrix([
[cos(thetaY), 0, sin(thetaY)],
[0, 1, 0],
[-sin(thetaY), 0, cos(thetaY)],
]),
matrix([
[cos(thetaZ), -sin(thetaZ), 0],
[sin(thetaZ), cos(thetaZ), 0],
[0, 0, 1],
]),
)
for (let i = 0; i < skills.length; i++) {
const currentPoint = points[i]
const newPoint = multiply(rotationMatrix, currentPoint)._data
newPoints.push(newPoint)
}
setPoints(newPoints)
setTriggerBy('rotateSphere')
}
const updateWindowDimensions = () => {
console.log('sphere', sphere)
try {
if (
sphereLimit !==
Math.min(sphere.current.clientHeight, sphere.current.clientWidth) / 2
) {
setSphereLimit(
Math.min(sphere.current.clientHeight, sphere.current.clientWidth) / 2,
)
fibSphere()
}
} catch (error) {
console.error(error)
}
}
const handleMouseMove = (e) => {
let xPosition = e.clientX
let yPosition = e.clientY
if (e.type === 'touchmove') {
xPosition = e.touches[0].pageX
yPosition = e.touches[0].pageY
}
const spherePosition = document
.getElementById('sphere')
.getBoundingClientRect()
const xDistance = xPosition - spherePosition.width / 2 - spherePosition.x
const yDistance = yPosition - spherePosition.height / 2 - spherePosition.y
const _xRatio = xDistance / sphereLimit
const _yRatio = yDistance / sphereLimit
setXRatio(_xRatio)
setYRatio(_yRatio)
setTriggerBy('ratios')
}
const addSphereItems = (ref) => {
setSphereItem((prev) => [...prev, ref])
}
return (
<motion.div
className="skills-body"
initial="initial"
animate="animate"
exit="exit"
custom={window}
variants={pageVariants}
transition={pageTransition}
onMouseMove={handleMouseMove}
onTouchMove={handleMouseMove}
>
<div className="skills-info-container">
<div className="skills-title">Skills</div>
<div className="skills-description">
I am a driven and passionate aspiring software engineer. I have
invested a significant amount of time and effort in self-teaching,
developing my knowledge and supporting others in the field of digital
technology. I thrive on the challenge of finding intelligent solutions
to complex problems and I am keen to apply and grow my skills in the
workplace.
</div>
</div>
<div className="sphere-container" id="sphere" ref={sphere}>
{isLoaded &&
skills.map((skill, index) => {
return (
<motion.div
ref={addSphereItems}
className="sphere-item"
key={index}
initial={{ opacity: 0 }}
animate={{
x:
sphereItem && sphereItem[index]
? points[index][0][0] - sphereItem[index].clientWidth / 2
: points[index][0][0],
y: points[index][1][0] - 20,
z: points[index][2][0],
opacity: Math.max(
(points[index][2][0] / sphereLimit + 1) / 2,
0.1,
),
}}
transition={{
duration: 0.1,
ease: 'linear',
}}
>
{skill}
</motion.div>
)
})}
</div>
</motion.div>
)
}
You seemed to be loosing the points
state value because you were triggering setPoints
from different parts of the your code in the same cycle.
What functional component does is that it batches the updates for one refresh cycle.
So the problem was that the functions fibSphere
and rotateSphere
were called in the same cycle, the first function fibSphere
althought did trigger setPoints
but the value for points did not change, since the function called after ( rotateSphere
) also triggered setPoints
. Both of these were batched.
So the order of operations, in order for your code to work was that once fibSphere
completed setting points, then (and only then) should rotateSphere
trigger and update points
.
I introduced the useEffect
(without dependancy array ie that triggers on each update) and made use of a state variable triggerBy
to actually see when each update happens on points
and execute the order of operations accordingly.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.