【问题标题】:How to correctly work with data from React Context in useEffect/useCallback-hook如何在 useEffect/useCallback-hook 中正确处理来自 React Context 的数据
【发布时间】:2020-01-18 11:34:29
【问题描述】:

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

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

虽然类中的一切都按预期工作,但我没有让它在功能组件中工作。

由于我的应用程序代码有点复杂,我创建了这个小示例 (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,而是继续计数。

问题在于,if (count.current === 5) { 行中的 count.current 始终是 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 异步完成的。

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

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

【问题讨论】:

    标签: javascript reactjs react-hooks react-context


    【解决方案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);};
    }, [])
    

    否则会出现内存泄漏

    【讨论】:

    • 我已经尝试过使用 useRef-hook 的类似解决方案,但我不知道我需要设置 countByRef.current = React.useContext(MyContext); 而不是简单地设置 const countByRef = React.useRef(React.useContext(MyContext));。感谢您澄清这一点!所以我假设这意味着,没有更简单的解决方案来访问上下文,而无需修改MyContextProvider 源代码,或使用useRef(),这会增加代码的一些开销?
    • @JensDuttke,您的延迟函数在声明时创建了闭包,useRef 和通过引用访问事物只是为了避免使用闭包。第三种选择是依赖闭包,在每次渲染时重新创建回调并使用setTimeout 而不是setInterval。但是它需要更多的代码来处理它,并且当值在计时器之外更新时,它还需要更多的代码来处理情况,因此您必须重新创建计时器(具有最新的回调和更小的延迟)。
    【解决方案2】:

    如果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>

    【讨论】:

    • 您的解决方案非常好,但没有什么办法可以解决MyComponent 组件中的问题吗?我对上下文的定义是错误的吗?我完全按照 React 文档中的做法进行操作:reactjs.org/docs/… 定义包含值的属性,并提供允许更改该值的方法。没有额外的方法来读取这个值,或者如果我使用 React Hooks 则需要这种方法的任何提示。
    • 钩子的主要思想是在重新渲染组件时重新创建所有内容,除非它被记忆(useMemouseCallback),或者是参考(useRef) .在这种情况下,memoized 函数依赖于不断变化的上下文。一种解决方案是使用 ref。另一个,是使useCallbac() 依赖于实际上不会改变的东西——来自上下文的函数。我已经稍微更新了答案以证明这一点。
    猜你喜欢
    • 2021-06-17
    • 1970-01-01
    • 2022-07-04
    • 2020-11-01
    • 2019-04-28
    • 1970-01-01
    • 2020-10-29
    • 2019-10-08
    • 2020-11-21
    相关资源
    最近更新 更多