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!