如何在useEffect / useCallback-hook

时间:2020-01-18 11:34:29

标签: javascript reactjs react-hooks react-context

我正在使用React Context存储数据并提供修改这些数据的功能。

现在,我正在尝试使用React Hooks将类组件转换为功能组件。

虽然一切都可以在Class中正常工作,但我无法在功能组件中正常工作。

由于我的应用程序代码稍微复杂一点,所以我创建了这个小示例(JSFiddle link),该示例可以重现该问题:

首先是上下文,这对于类和功能组件都是相同的:

const MyContext = React.createContext();

class MyContextProvider extends React.Component {
    constructor (props) {
        super(props);

        this.increase = this.increase.bind(this);
        this.reset = this.reset.bind(this);

        this.state = {
            current: 0,
            increase: this.increase,
            reset: this.reset
        }
    }

    render () {
        return (
            <MyContext.Provider value={this.state}>
                {this.props.children}
            </MyContext.Provider>
        );
    }

    increase (step) {
        this.setState((prevState) => ({
            current: prevState.current + step
        }));
    }

    reset () {
        this.setState({
            current: 0
        });
    }
}

现在,这是Class组件,可以正常工作:

class MyComponent extends React.Component {
    constructor (props) {
        super(props);

        this.increaseByOne = this.increaseByOne.bind(this);
    }

    componentDidMount () {
        setInterval(this.increaseByOne, 1000);
    }

    render () {
        const count = this.context;

        return (
            <div>{count.current}</div>
        );
    }

    increaseByOne () {
        const count = this.context;

        if (count.current === 5) {
            count.reset();
        }
        else {
            count.increase(1);
        }
    }
}
MyComponent.contextType = MyContext;

预期结果是,以一秒的间隔计数到5,然后从0重新开始。

这是转换后的功能组件:

const MyComponent = (props) => {
    const count = React.useContext(MyContext);

    const increaseByOne = React.useCallback(() => {
        console.log(count.current);

        if (count.current === 5) {
            count.reset();
        }
        else {
            count.increase(1);
        }
    }, []);

    React.useEffect(() => {
        setInterval(increaseByOne, 1000);
    }, [increaseByOne]);

    return (
        <div>{count.current}</div>
    );
}

与其重新将计数器重置为5,不如恢复计数。

问题在于,count.current行中的if (count.current === 5) {始终为0,因为它不使用最新值。

我要使其正常工作的唯一方法是按以下方式调整代码:

const MyComponent = (props) => {
    const count = React.useContext(MyContext);

    const increaseByOne = React.useCallback(() => {
        console.log(count.current);

        if (count.current === 5) {
            count.reset();
        }
        else {
            count.increase(1);
        }
    }, [count]);

    React.useEffect(() => {
        console.log('useEffect');

        const interval = setInterval(increaseByOne, 1000);

        return () => {
            clearInterval(interval);
        };
    }, [increaseByOne]);

    return (
        <div>{count.current}</div>
    );
}

现在,increaseByOne回调将在每次上下文更改时重新创建,这也意味着每秒都会调用该效果。
结果是,每次对上下文进行更改时,它都会清除间隔并设置一个新间隔(您可以在浏览器控制台中看到)。
在这个小示例中,这可能会起作用,但是它改变了原来的逻辑,并且开销更大。

我的应用程序不依赖于间隔,但它正在监听事件。删除事件侦听器并在以后再次添加它,这意味着,如果在删除和侦听器绑定之间触发了某些事件,则我可能会放开一些事件,这是由React异步完成的。

有人在不改变一般逻辑的情况下有一个想法,期望如何解决这个问题?

我在这里创建了一个小提琴,以处理上面的代码:
https://jsfiddle.net/Jens_Duttke/78y15o9p/

2 个答案:

答案 0 :(得分:1)

如果increaseByOne函数不需要知道实际的count.current,则可以避免重新创建它。在上下文中,创建一个名为is的新函数,该函数检查current是否等于一个值:

is = n => this.state.current === n;

并在increaseByOne函数中使用此函数:

if (count.is(5)) {
    count.reset();
}

示例:

const MyContext = React.createContext();

class MyContextProvider extends React.Component {
  render() {
    return (
      <MyContext.Provider value={this.state}>
        {this.props.children}
      </MyContext.Provider>
    );
  }

  increase = (step) => {
    this.setState((prevState) => ({
      current: prevState.current + step
    }));
  }

  reset = () => {
    this.setState({
      current: 0
    });
  }

  is = n => this.state.current === n;

  state = {
    current: 0,
    increase: this.increase,
    reset: this.reset,
    is: this.is
  };
}

const MyComponent = (props) => {
  const { increase, reset, is, current } = React.useContext(MyContext);

  const increaseByOne = React.useCallback(() => {
    if (is(5)) {
      reset();
    } else {
      increase(1);
    }
  }, [increase, reset, is]);

  React.useEffect(() => {
    setInterval(increaseByOne, 1000);
  }, [increaseByOne]);

  return (
    <div>{current}</div>
  );
}

const App = () => (
  <MyContextProvider>
    <MyComponent />
  </MyContextProvider>
);

ReactDOM.render( <
  App / > ,
  document.querySelector("#app")
);
body {
  background: #fff;
  padding: 20px;
  font-family: Helvetica;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="app"></div>

答案 1 :(得分:1)

第一个解决方案是将随时间变化的数据放入useRef中,这样就可以通过引用而不是闭包来访问它(以及在基于类的版本中访问实际的this.state

const MyComponent = (props) => {
  const countByRef = React.useRef(0);
    countByRef.current = React.useContext(MyContext);

    React.useEffect(() => {
        setInterval(() => {
          const count = countByRef.current;

          console.log(count.current);

          if (count.current === 5) {
                count.reset();
          } else {
            count.increase(1);
          }
      }, 1000);
    }, []);

    return (
        <div>{countByRef.current.current}</div>
    );
}

另一种解决方案是修改resetincrease以允许函数自变量,并且可以使用setStateuseState的更新程序来实现。

那应该是

useEffect(() => {
  setInterval(() => {
    count.increase(current => current === 5? 0: current + 1);
  }, 1000);
}, [])

PS还希望您不要错过实际代码中的清理功能:

useEffect(() => {
 const timerId = setInterval(..., 1000);
 return () => {clearInterval(timerId);};
}, [])

否则,您将发生内存泄漏