lulusir / my-blog

my-blog
https://github.com/lulusir/my-blog/issues
13 stars 1 forks source link

记录使用Hooks遇到的问题 #19

Open lulusir opened 5 years ago

lulusir commented 5 years ago

定时器没有获取到最新的state

定时器中的count 会一直会0, 因为定时器一直引用着旧的变量。 demo

function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => {
      // 下面的count 会一直为0,因为定时器一直引用着旧的变量。
      // 
      console.log(count);
      setCount(precount => precount + 1);
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []);
  return <div className="App">{count}</div>;
}

解决方法:
用一个变量来保存引用值,在每次react执行时更新。既然使用hook,我们可以用useRef来保存这个引用 demo

function App() {
  const [count, setCount] = useState(0);
  const countRef = useRef(null);
  // 更新引用值,
  countRef.current = count;
  useEffect(() => {
    const timer = setInterval(() => {
      // 这里的引用值会定期更新
      console.log(count, countRef.current, " == count, countRef.current");
      setCount(precount => precount + 1);
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []);
  return <div className="App">{count}</div>;
}

或者在外层作用域保存这个变量(注意这里会出现多个组件引用同一个变量的问题:参考3楼的demo)

let val = 0
function App() {
  const [count, setCount] = useState(0);
  // 更新引用值,
  val = count;
  useEffect(() => {
    const timer = setInterval(() => {
      // 这里的引用值会定期更新
      console.log(count, val, " == count, obj.current");
      setCount(precount => precount + 1);
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []);
  return <div className="App">{count}</div>;
}

我们可以把这个功能包装成hooks,demo

function useRefState(initialState) {
  const [state, setState] = useState(initialState);
  const stateRef = useRef(null);
  stateRef.current = state;
  return [state, setState, stateRef];
}

function App() {
  const [count, setCount, countRef] = useRefState(0);
  useEffect(() => {
    const timer = setInterval(() => {
      // 这里的引用值会定期更新
      console.log(count, countRef.current, " == count, countRef.current");
      setCount(precount => precount + 1);
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []);
  return <div className="App">{count}</div>;
}

参考 https://github.com/facebook/react/issues/14543

liyuanqiu commented 5 years ago

函数每次执行都更新 current 确实没问题,但是从更加精确的控制角度来讲,是不是这样会更好:

useEffect(() => {
  stateRef.current = state;
}, [state]);

let val = 0;

在函数组建作用域外使用可变变量,会导致该组件同时只能使用一个。

lulusir commented 5 years ago

@liyuanqiu 嗯,像你这样比对更新的确会更好。

function useRefState(initialState) {
  const [state, setState] = useState(initialState);
  const stateRef = useRef(null);
  useEffect(() => {
    stateRef.current = state;
  }, [state]);
  return [state, setState, stateRef];
}

在函数组建作用域外使用可变变量,会导致该组件同时只能使用一个。

这个是什么意思?

liyuanqiu commented 5 years ago

A Button component:

let sum = 0;
function Button({
  name,
}) {
  function handleClick() {
    sum += 1;
    console.log(`Button(${name}) has been clicked for ${sum} times!`);
  }
  return (
    <button
      onClick={handleClick}
    >
      click me!
    </button>
  );
}

In your app:

function App() {
  return (
    <Button name="button-1" />
    <Button name="button-2" />
  );
}

See: https://codesandbox.io/s/w0rjxro1q7 (Open console to see logs)

hzfvictory commented 4 years ago

第一个例子 像这样 setCount(precount => precount + 1); 他拿到的是上一次的count值,他会一直改变count ,虽然定时器里面的count,不会改变,但是页面上的值,会一直跟随者定时器来更新

第二个例子你使用了useRef()

 setCount(countRef.current + 1);

你大可不必用函数的方式,因为这种是间接拿到的是count 值

import React, {Fragment, useState, useEffect, useRef, useCallback} from "react";

const useInterval = (callback: any, val: any) => {
    const saveCallback = useRef();
    useEffect(() => {
        saveCallback.current = val
    },[val]);

    useEffect(() => {
        let id = setInterval(() => callback(saveCallback.current), 1000);
        return () => clearInterval(id);
    }, []);
};

function App() {
    const [count, setCount] = useState(0);

    useInterval((val: number) => {
        setCount(val + 1);
    }, count);

    return (
        <Fragment>
            <p> count: {count} </p>
        </Fragment>
    );
}

export default App