有类到功能组件迁移的 React 状态问题

React State Issues with Classful to Functional Component Migration

问题

我正在将有类组件转换为功能组件,但状态重新渲染存在一些问题。

这是原始的有类组件:

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) {
    // 
    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>
    );
  }
}

它本质上是一个根据鼠标移动而移动的单词球体demo

现在,这是我迁移到功能组件所取得的进展:

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) => {
    // 
    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>
  );
}

调查

现在,当我 运行 这个功能版本时,组件的每个状态更新似乎是 'reset',而不是更新 UI,这里是 codesandbox env

在浏览器中高亮'skill'其中一个单词时,它似乎在非常快速地切换长度(每100ms,与旋转球体相同的间隔)。这可以通过进入开发工具并查看每个 'skill' 字每 100 毫秒更改一次来确认。

除非我弄错了,否则这似乎根本不对。功能组件中的 skills 变量是 const,所以不应该随状态变化而改变吗?

我觉得我遗漏了一些非常明显的东西,感谢任何帮助!

这是您的 class 组件的工作部分(功能组件):

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 = () => {
    // 
    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>
  )
}

您似乎正在丢失 points 状态值,因为您在同一周期内从代码的不同部分触发 setPoints

功能组件的作用是对一个刷新周期的更新进行批处理。

所以问题是函数 fibSphererotateSphere 在同一个循环中被调用,第一个函数 fibSphere 虽然触发了 setPoints 但值点没有改变,因为在 (rotateSphere) 之后调用的函数也触发了 setPoints。这两个都是批处理的。

因此,为了让您的代码正常工作,操作顺序是一旦 fibSphere 完成设置点,然后(并且只有那时)应该 rotateSphere 触发并更新 points .

我引入了useEffect(没有依赖数组,即在每次更新时触发)并使用状态变量 triggerBy 来实际查看每次更新何时发生在 points 和相应地执行操作顺序。