简体   繁体   English

如何在 useEffect/useCallback-hook 中正确处理来自 React Context 的数据

[英]How to correctly work with data from React Context in useEffect/useCallback-hook

I'm using a React Context to store data and to provide functionality to modify these data.我正在使用 React Context 来存储数据并提供修改这些数据的功能。

Now, I'm trying to convert a Class Component into a Functional Component using React Hooks.现在,我正在尝试使用 React Hooks 将类组件转换为功能组件。

While everything is working as expected in the Class, I don't get it to work in the Functional Component.虽然一切都在类中按预期工作,但我没有让它在功能组件中工作。

Since my applications code is a bit more complex, I've created this small example ( JSFiddle link ), which allows to reproduce the problem:由于我的应用程序代码有点复杂,我创建了这个小示例( JSFiddle link ),它允许重现问题:

First the Context, which is the same for both, the Class and the Functional Component:首先是上下文,对于类和功能组件都是相同的:

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

Now, here is the Class component, which works just fine:现在,这是 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;

The expected result is, that it counts to 5, in an interval of one second - and then starts again from 0.预期的结果是,它以一秒的间隔计数到 5 - 然后再次从 0 开始。

And here is the converted Functional Component:这是转换后的功能组件:

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

Instead of resetting the counter at 5, it resumes counting.它不是将计数器重置为 5,而是恢复计数。

The problem is, that count.current in line if (count.current === 5) { is always 0 , since it does not use the latest value.问题是,这个count.current in line if (count.current === 5) {总是0 ,因为它不使用最新的值。

The only way I get this to work, is to adjust the code on the following way:我让它工作的唯一方法是按以下方式调整代码:

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

Now, the increaseByOne callback is recreated on every change of the context, which also means that the effect is called every second.现在,在每次上下文更改时都会重新创建increaseByOne回调,这也意味着每秒钟都会调用该效果。
The result is, that it clears the interval and sets a new one, on every change to the context (You can see that in the browser console).结果是,它在每次上下文更改时清除间隔并设置新的间隔(您可以在浏览器控制台中看到)。
This may work in this small example, but it changed the original logic, and has a lot more overhead.这可能在这个小例子中工作,但它改变了原来的逻辑,并且有更多的开销。

My application does not rely on an interval, but it's listening for an event.我的应用程序不依赖于间隔,但它正在侦听事件。 Removing the event listener and adding it again later, would mean, that I may loose some events, if they are fired between the remove and the binding of the listener, which is done asynchronously by React.删除事件侦听器并稍后再次添加它,意味着我可能会丢失一些事件,如果它们在侦听器的移除和绑定之间被触发,这是由 React 异步完成的。

Has someone an idea, how it is expected to React, to solve this problem without to change the general logic?有没有人有想法,期望React如何在不改变一般逻辑的情况下解决这个问题?

I've created a fiddle here, to play around with the code above:我在这里创建了一个小提琴,以玩弄上面的代码:
https://jsfiddle.net/Jens_Duttke/78y15o9p/ https://jsfiddle.net/Jens_Duttke/78y15o9p/

First solution is to put data is changing through time into useRef so it would be accessible by reference not by closure(as well as you access actual this.state in class-based version)第一个解决方案是将随时间变化的数据放入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>
    );
}

Another solution is to modify reset and increase to allow functional argument as well as it's possible with setState and useState 's updater.另一种解决方案是修改resetincrease以允许功能参数以及setStateuseState的更新程序。

Then it would be然后就是

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

PS also hope you have not missed clean up function in your real code: PS 还希望您没有错过实际代码中的清理功能:

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

otherwise you will have memory leakage否则你会有内存泄漏

If the increaseByOne function doesn't need to know the actual count.current , you can avoid recreating it.如果increaseByOne函数不需要知道实际的count.current ,则可以避免重新创建它。 In the context create a new function called is that checks if the current is equal a value:在上下文中创建一个名为is的新函数,用于检查current是否等于一个值:

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

And use this function in the increaseByOne function:并在increaseByOne函数中使用这个函数:

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

Example:例子:

 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>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 React Hook useEffect/useCallback 缺少依赖项 - React Hook useEffect/useCallback has a missing dependency 如何正确使用 setInterval 和 react useEffect 挂钩? - How to use setInterval with react useEffect hook correctly? 在 React 中使用 useEffect() 和 useCallback 获取数据 - Data Fetching Using useEffect() And useCallback In React 当 useCallback 缺少依赖时 React useState 和 useCallback hook 如何工作 - How does React useState and useCallback hook work when useCallback lacks dependencies 使用 React 的 useCallback 钩子代替 useEffect 的意图是什么? - What is the intention of using React's useCallback hook in place of useEffect? 如何正确地将事件侦听器添加到 React useEffect 挂钩? - How to correctly add event listener to React useEffect hook? 在 useCallback() 钩子中反应 setState 没有正确设置状态变量? - React setState inside useCallback() hook not setting state variable correctly? 数据未通过 React Hook useEffect() 更新 - Data is not updating with React Hook useEffect() 与useEffect一起使用时如何防止触发useCallback(并遵守eslint-plugin-react-hooks)? - How to prevent useCallback from triggering when using with useEffect (and comply with eslint-plugin-react-hooks)? 如何正确使用 Promise、Firebase 和 React 的 UseEffect 钩子? - How to properly work with promises, Firebase and React's UseEffect hook?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM