最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to correctly work with data from React Context in useEffectuseCallback-hook - Stack Overflow

programmeradmin4浏览0评论

I'm using a React Context to store data and to provide functionality to modify these data.

Now, I'm trying to convert a Class Component into a Functional Component using 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 plex, I've created this small example (JSFiddle link), which allows to reproduce the problem:

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 ponent, which works just fine:

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

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

    ponentDidMount () {
        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.

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.

The problem is, that count.current in line if (count.current === 5) { is always 0, since it does not use the latest value.

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.
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.

Has someone an idea, how it is expected to React, to solve this problem without to change the general logic?

I've created a fiddle here, to play around with the code above:
/

I'm using a React Context to store data and to provide functionality to modify these data.

Now, I'm trying to convert a Class Component into a Functional Component using 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 plex, I've created this small example (JSFiddle link), which allows to reproduce the problem:

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 ponent, which works just fine:

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

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

    ponentDidMount () {
        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.

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.

The problem is, that count.current in line if (count.current === 5) { is always 0, since it does not use the latest value.

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.
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.

Has someone an idea, how it is expected to React, to solve this problem without to change the general logic?

I've created a fiddle here, to play around with the code above:
https://jsfiddle/Jens_Duttke/78y15o9p/

Share asked Jan 18, 2020 at 11:34 user4449804user4449804
Add a ment  | 

2 Answers 2

Reset to default 4

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)

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.

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:

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. In the context create a new function called is that checks if the current is equal a value:

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

And use this function in the increaseByOne function:

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./react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg./react-dom@16/umd/react-dom.development.js"></script>

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

发布评论

评论列表(0)

  1. 暂无评论