简体   繁体   中英

clearInterval() not working in React Native - trying to convert from class to functional components - Timer

Trying to make a stopwatch in react native I have the working code with class components but when trying with functional components the clearInterval() function doesn't work

source code from ReactNativeAcademy/Stopwatch before modifications here https://github.com/ReactNativeAcademy/Stopwatch/blob/master/App.js

I need just a basic timer without laps with only start / resume / stop / reset buttons snack url for my code: https://snack.expo.io/@mansouriala/nervous-mixed-nuts in order to test it in class based components you can find each class based function commented under the functional one.

I don't know but maybe one solution for that could be to wrap setInterval in a useEffect then make a new state variable toggle it to true when start and the useEffect listens to that variable.

without further ado here's the code:

import React, { Component, useEffect, useState } from 'react';
import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';
import moment from 'moment';

function Timer({ interval, style }) {
  const pad = (n) => (n < 10 ? `0${n}` : n);
  const duration = moment.duration(interval);
  const centiseconds = Math.floor(duration.milliseconds() / 10);

  return (
    <View style={styles.timerContainer}>
      <Text style={style}>{pad(duration.minutes())}:</Text>
      <Text style={style}>{pad(duration.seconds())},</Text>
      <Text style={style}>{pad(centiseconds)}</Text>
    </View>
  );
}

function RoundButton({ title, color, background, onPress }) {
  return (
    <TouchableOpacity onPress={onPress} style={[styles.button, { backgroundColor: background }]}>
      <View style={styles.buttonBorder}>
        <Text style={[styles.buttonTitle, { color }]}>{title}</Text>
      </View>
    </TouchableOpacity>
  );
}

function ButtonsRow({ children }) {
  return <View style={styles.buttonsRow}>{children}</View>;
}

// export default class App extends Component {
export default function App() {
  const [timer, setTimer] = useState(0);
  const [state, setState] = useState({
    start: 0,
    now: 0,
    currentTime: 0,
  });

  // constructor(props) {
  //   super(props);
  //   this.state = {
  //     start: 0,
  //     now: 0,
  //     currentTime: 0,
  //   };
  // }

  useEffect(() => {
    return () => {
      clearInterval(timer);
    };
  }, []);

  // componentWillUnmount() {
  //   clearInterval(this.timer);
  // }

  const startHandler = () => {
    const now = new Date().getTime();
    setState({
      start: now,
      now,
      currentTime: 0,
    });
    setInterval(
      setInterval(() => {
        setState((prev) => ({ ...prev, now: new Date().getTime() }));
      }, 100)
    );
  };

  // startHandler = () => {
  //   const now = new Date().getTime();
  //   this.setState({
  //     start: now,
  //     now,
  //     currentTime: 0,
  //   });
  //   this.timer = setInterval(() => {
  //     this.setState({ now: new Date().getTime() });
  //   }, 100);
  // };

  const stopHandler = () => {
    clearInterval(timer);
    const { currentTime, now, start } = state;
    setState((prev) => ({
      // ...prev,
      currentTime: currentTime + (now - start),
      start: 0,
      now: 0,
    }));
  };

  // stopHandler = () => {
  //   clearInterval(this.timer);
  //   const { currentTime, now, start } = this.state;
  //   this.setState({
  //     currentTime: currentTime + now - start,
  //     start: 0,
  //     now: 0,
  //   });
  // };

  const resetHandler = () => {
    setState({
      currentTime: 0,
      start: 0,
      now: 0,
    });
  };

  // resetHandler = () => {
  //   this.setState({
  //     currentTime: 0,
  //     start: 0,
  //     now: 0,
  //   });
  // };

  const resumeHandler = () => {
    const now = new Date().getTime();
    setState({
      start: now,
      now,
    });
    setTimer(
      setInterval(() => {
        setState((prev) => ({ ...prev, now: new Date().getTime() }));
      }, 100)
    );
  };

  // resumeHandler = () => {
  //   const now = new Date().getTime();
  //   this.setState({
  //     start: now,
  //     now,
  //   });
  //   this.timer = setInterval(() => {
  //     this.setState({ now: new Date().getTime() });
  //   }, 100);
  // };

  // render() {
  const { now, start, currentTime } = state;
  // const { now, start, currentTime } = this.state;

  return (
    <View style={styles.container}>
      <Timer interval={currentTime + (now - start)} style={styles.timer} />
      <ButtonsRow>
        <RoundButton title={'Start'} color={'#50D167'} background={'#1B361F'} onPress={startHandler} />
        <RoundButton title={'Stop'} color={'#E33935'} background={'#3C1715'} onPress={stopHandler} />
        <RoundButton title={'Reset'} color={'#FFFFFF'} background={'#3D3D3D'} onPress={resetHandler} />
        <RoundButton title={'Resume'} color={'#50D167'} background={'#1B361F'} onPress={resumeHandler} />
      </ButtonsRow>
    </View>
  );
}
// }

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#0D0D0D',
    alignItems: 'center',
    paddingTop: 130,
    paddingHorizontal: 20,
  },
  timer: {
    color: '#FFFFFF',
    fontSize: 76,
    fontWeight: '200',
    width: 110,
  },
  button: {
    width: 80,
    height: 80,
    borderRadius: 40,
    justifyContent: 'center',
    alignItems: 'center',
  },
  buttonTitle: {
    fontSize: 18,
  },
  buttonBorder: {
    width: 76,
    height: 76,
    borderRadius: 38,
    borderWidth: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  buttonsRow: {
    flexDirection: 'row',
    alignSelf: 'stretch',
    justifyContent: 'space-between',
    marginTop: 80,
    marginBottom: 30,
  },
  timerContainer: {
    flexDirection: 'row',
  },
});

Try it:

useEffect(() => {    
  clearInterval(timer);  
}, []);

When using return inside useEffect, the code is only triggered when component unmount.

Don't store something like an interval id in state, as re-renders occur each update. If you're functional, implement the setInterval with useRef() , if class based, use this.interval .

Another gotcha is calling clearInterval() in a functional component on the ref, instead of .current

heres a snippet from what i just debugged:

  const spinnerCount = useRef(0)
  const interval = useRef(null)

  useEffect(() => {
    if (withProgress && inProgress && notification == '') {
      interval.current = setInterval(() => {
        if (spinnerCount.current >= 40) {
          clearInterval(interval.current)
          spinnerCount.current = 0
          setNotification('Something happened... Please try again.')
        } else {
          spinnerCount.current = spinnerCount.current + 1
        }
      }, 1000)
    } 
    if (notification !== '' && inProgress === false) {
      const delay = notification.length > 100 ? 6000 : 3000
      setTimeout(() => {
        clearInterval(interval.current)
        spinnerCount.current = 0
        setNotification('');
      }, delay);
    }
  }, [inProgress])

Theres a little extra in there, basically this is a disappearing notification component, that also features a progress spinner. in this case, if the component is displaying the spinner, but a success/error notification is never triggered, the spinner will auto-quit after 40 seconds. hence the interval/spinnerCount

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM