挂钩返回的函数中的状态不变

时间:2020-06-30 00:20:02

标签: reactjs react-hooks

我正在玩React和Canvas。我想通过requestAnimationFrame为正方形的移动制作动画。因此,我制作了用于处理画布元素和更新正方形位置的主要组件:

function App() {
    let canvas = useRef()
    let square = useSquare(WIDTH / 2, HEIGHT, 2)
    let update = () => {
        square.update()
        requestAnimationFrame(update)
    }
    useEffect(update, [])
    return <>
        <canvas ref={canvas} width={WIDTH} height={HEIGHT} />
        <Renderer.Provider value={() => ref.canvas.getContext("2d")}>
            <Square {...square} />
        </Renderer.Provider>
    </>
}

Square是一个组件,但不返回JSX。它在useEffect上运行绘图操作:

function Square({ x, y }) {
    let getContext = useContext(Renderer)
    useEffect(() => {
        let ctx = getContext()
        ctx.fillStyle = "#000"
        ctx.fillRect(x, y, 10, 10)
    }, [x, y])
    return null;
}

我想用钩子封装正方形的状态和行为:

function useSquare(init_x, init_y) {
    let [x, set_x] = useState(init_x)
    let [y, set_y] = useState(init_y)
    let [vx, set_vx] = useState(10)
    let [vy, set_vy] = useState(-5)
    let update = () => {
        set_x(prev_x => prev_x + vx)
        set_y(prev_y => prev_y + vy)
        if (x <= 0 || x >= WIDTH) set_vx(prev_vx => -prev_vx)
        else if (y <= 0 || y >= HEIGHT) set_vy(prev_vy => -prev_vy)
    }
    return { update, x, y }
}

对我来说似乎很合理。但是,它没有按预期工作。正方形正在移动,但对与视口边界的碰撞没有反应。

如果我将console.log(x, y)放到update两个函数中,我可以看到它以预期的速率触发,但是状态没有改变。

我不明白为什么状态不变时组件能够正确呈现。坦白说,我不知道我在做什么错。也许有人帮我澄清一下。预先感谢。

PS:我用示例https://codesandbox.io/s/xenodochial-jepsen-nfxv6

制作了一个代码编辑器

2 个答案:

答案 0 :(得分:1)

答案1 :问题是每次requestAnimationFrame运行时,square.update()都是您实际创建的新框。您需要先清除边框,然后再绘制新边框

import { useContext, useEffect } from "react";
import { Renderer } from "./ctx";

export function Square({ x, y }) {
  let getContext = useContext(Renderer);
  useEffect(() => {
    let ctx = getContext();
    // clear before drawing
    ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
    ctx.fillStyle = "#000";
    ctx.fillRect(x, y, 10, 10);
  }, [x, y]);
  return null;
}

检查codesandbox是否可视化

这是我在网上找到的a very good series。看看

答案2:requestAnimationFrame创建一个调用square.update()的循环,这就是为什么您可以看到尺寸已更改的原因

答案3:您正在通过调用Square

来更新set**组件内的状态

答案 1 :(得分:1)

问题是从update返回的useSquare函数是一个闭包,并且其中的xy的值永远不会改变,因此{{ 1}}和x <= 0 || x >= WIDTH永远都不正确。这是关闭的示例:

y <= 0 || y >= HEIGHT

与关闭相关的错误很难被识别。虽然您可以在包括MDN的各种资源中了解有关闭包的更多信息,但是可以使用let x = 1; function update(y) { return function() { console.log(y); // always 1 }; } setInterval(update(x), 1000); x = 2;包中的exhaustive-deps linting规则来确保正确指定了在挂钩中指定的依赖项数组,并防止了错误。 React docs(默认情况下,在CodeSandbox中启用了此规则。)

但是在此之前,让我们简化任务以找到解决问题的更好方法。假设有一个状态eslint-plugin-react-hooks会一直递增3,直到达到20。然后它将保持相同的值递减,直到达到0。

在这种情况下,我们使用+ 1 / -1表示增量/减量的方向。由于此方向更改不应触发重新渲染,因此我们不必将其存储为状态变量。相反,我们将其存储为ref,因为我们希望在计算新的x时访问其最新值。为了检测x是否达到限制,我们设置了一个x挂钩。完整的实现将是:

useEffect

回到您的情况,应该更新的地方很少,主要部分在import React, { useState, useRef, useEffect } from "react"; const MAX = 20; const RATE = 3; function App() { let [x, setX] = useState(0); const direction = useRef(1); useEffect(() => { const id = setInterval(() => { setX(val => val + direction.current * RATE); }, 500); return () => clearInterval(id); }, []); useEffect(() => { if (x >= MAX) direction.current = -1; if (x <= 0) direction.current = 1; }, [x]); return <div>{x}</div>; } 内部。像上面的示例一样,我们不再将useSquarevx存储为状态。而是使用vy初始化它们。还设置了两个useRef挂钩,以根据useEffectx的值更改其值。

y

// useSquare.js import { useState, useRef, useEffect, useCallback } from "react"; import { WIDHT, HEIGHT } from "./settings"; export function useSquare(init_x, init_y) { let [x, set_x] = useState(init_x); let [y, set_y] = useState(init_y); let vx = useRef(10); let vy = useRef(-5); let update = useCallback(() => { set_x(prev_x => prev_x + vx.current); set_y(prev_y => prev_y + vy.current); }, []); useEffect(() => { if (x >= WIDHT) vx.current = -10; if (x <= 0) vx.current = 10; }, [x]); useEffect(() => { if (y <= 0) vy.current = 5; if (y >= HEIGHT) vy.current = -5; }, [y]); useEffect(() => { let id; function cb() { update(); id = requestAnimationFrame(cb); } cb(); return () => cancelAnimationFrame(id); }, [update]); return { x, y }; } 变为:

app.js

注意,我用function App() { let canvas = useRef(); let square = useSquare(WIDHT / 2, HEIGHT / 2); return ( <> <canvas ref={canvas} width={WIDHT} height={HEIGHT} /> <Renderer.Provider value={() => canvas.current.getContext("2d")}> <Square {...square} /> </Renderer.Provider> </> ); } 钩子包装了更新函数,以使其不会随useCallbackx值而改变。我还将动画功能放在y挂钩中。如果将其移回useSquare,请确保您正确设置了依赖项数组。

这是更新的沙箱:

Edit state-is-unchanged-from-within-a-function-returned-by-a-hook