通过从类到功能组件迁移来反应状态问题
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要迅速切换长度(每100毫米,与旋转球体相同的间隔)。可以通过进入开发工具并看到每个“技能”单词每100毫秒每100ms更改一次来确认这一点。
除非我错了,否则这似乎根本不对。功能组件中的技能变量是const
,因此不应该改变状态更改吗?
我觉得我缺少一些很明显的东西,任何帮助!
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!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
这是类组件的一个工作部分(功能组件):
您似乎正在丢失
points
状态值,因为您从代码的不同部分触发setPoints
同一个周期。功能组件的作用是在一个刷新周期内批量更新。
所以问题是函数
fibSphere
和rotateSphere
在同一个周期中调用,第一个函数fibSphere
虽然确实触发了setPoints< /code> 但点的值没有改变,因为 (
rotateSphere
) 之后调用的函数也触发了setPoints
。这两个都是分批的。因此,为了让您的代码正常工作,操作的顺序是,一旦
fibSphere
完成设置点,那么(并且只有那时)应该rotateSphere
触发并更新点
。我引入了
useEffect
(没有依赖数组,即在每次更新时触发),并利用状态变量triggerBy
来实际查看每次更新何时发生在点< /code> 并相应地执行操作顺序。
Here is a working piece (functional component) of your class component:
You seemed to be loosing the
points
state value because you were triggeringsetPoints
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
androtateSphere
were called in the same cycle, the first functionfibSphere
althought did triggersetPoints
but the value for points did not change, since the function called after (rotateSphere
) also triggeredsetPoints
. 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) shouldrotateSphere
trigger and updatepoints
.I introduced the
useEffect
(without dependancy array i.e. that triggers on each update) and made use of a state variabletriggerBy
to actually see when each update happens onpoints
and execute the order of operations accordingly.