Atualizar o componente React a cada segundo

114

Tenho brincado com o React e tenho o seguinte componente de tempo que apenas renderiza Date.now()na tela:

import React, { Component } from 'react';

class TimeComponent extends Component {
  constructor(props){
    super(props);
    this.state = { time: Date.now() };
  }
  render(){
    return(
      <div> { this.state.time } </div>
    );
  }
  componentDidMount() {
    console.log("TimeComponent Mounted...")
  }
}

export default TimeComponent;

Qual seria a melhor maneira de fazer com que esse componente fosse atualizado a cada segundo para redesenhar o tempo de uma perspectiva do React?

matador de floco de neve
fonte

Respostas:

152

Você precisa usar setIntervalpara acionar a mudança, mas também precisa limpar o cronômetro quando o componente é desmontado para evitar que ele deixe erros e vaze memória:

componentDidMount() {
  this.interval = setInterval(() => this.setState({ time: Date.now() }), 1000);
}
componentWillUnmount() {
  clearInterval(this.interval);
}
Waiski
fonte
2
Se você quiser uma biblioteca que encapsula esse tipo de coisa, eu fizreact-interval-rerender
Andy
Eu adicionei meu método setInterval em meu construtor e funcionou melhor.
aqteifan
89

O código a seguir é um exemplo modificado do site React.js.

O código original está disponível aqui: https://reactjs.org/#a-simple-component

class Timer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      seconds: parseInt(props.startTimeInSeconds, 10) || 0
    };
  }

  tick() {
    this.setState(state => ({
      seconds: state.seconds + 1
    }));
  }

  componentDidMount() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  formatTime(secs) {
    let hours   = Math.floor(secs / 3600);
    let minutes = Math.floor(secs / 60) % 60;
    let seconds = secs % 60;
    return [hours, minutes, seconds]
        .map(v => ('' + v).padStart(2, '0'))
        .filter((v,i) => v !== '00' || i > 0)
        .join(':');
  }

  render() {
    return (
      <div>
        Timer: {this.formatTime(this.state.seconds)}
      </div>
    );
  }
}

ReactDOM.render(
  <Timer startTimeInSeconds="300" />,
  document.getElementById('timer-example')
);
Sr. Polywhirl
fonte
1
Eu, por algum motivo, recebo this.updater.enqueueSetState não é um erro de função ao usar essa abordagem. O retorno de chamada setInterval está devidamente vinculado ao componente this.
baldrs
16
Para idiotas como eu: não nomeie o cronômetro updaterporque isso estraga o ciclo de atualização
baldrs
3
Com ES6 eu preciso mudar uma linha como this.interval = setInterval (this.tick.bind (this), 1000);
Kapil
Você pode adicionar o link ao url que faz referência? Obrigado ~
frederj
Eu adicionei um método de formatação e uma propriedade "construtor" para maior robustez.
Sr. Polywhirl
15

@Waisky sugeriu:

Você precisa usar setIntervalpara acionar a mudança, mas também precisa limpar o cronômetro quando o componente é desmontado para evitar que ele deixe erros e vaze memória:

Se você gostaria de fazer a mesma coisa, usando ganchos:

const [time, setTime] = useState(Date.now());

useEffect(() => {
  const interval = setInterval(() => setTime(Date.now()), 1000);
  return () => {
    clearInterval(interval);
  };
}, []);

Em relação aos comentários:

Você não precisa passar nada dentro []. Se passar timeentre colchetes, significa executar o efeito toda vez que o valor de timemudar, ou seja, invoca um novo a setIntervalcada vez, timemuda, que não é o que estamos procurando. Queremos invocar apenas setIntervaluma vez quando o componente é montado e, em seguida, setIntervalchamasetTime(Date.now()) cada 1000 segundos. Finalmente, invocamos clearIntervalquando o componente é desmontado.

Observe que o componente é atualizado, com base em como você o usou time, toda vez que o valor de timemudar. Isso não tem nada a ver com colocar timeem []de useEffect.

1 homem
fonte
1
Por que você passou um array vazio ( []) como segundo argumento para useEffect?
Mekeor Melire
A documentação do React no gancho de efeito nos diz: “Se você deseja executar um efeito e limpá-lo apenas uma vez (na montagem e desmontagem), você pode passar um array vazio ([]) como um segundo argumento.” Mas o OP deseja que o efeito seja executado a cada segundo, ou seja, recorrente, ou seja, não apenas uma vez.
Mekeor Melire
A diferença que percebi se você passar [tempo] no array, isso vai criar e destruir o componente a cada intervalo. Se você passar o array vazio [], ele continuará atualizando o componente e só o desmontará quando você sair da página. Mas, em qualquer caso, para fazê-lo funcionar, eu tive que adicionar key = {time} ou key = {Date.now ()} para que realmente fosse renderizado novamente.
Teebu
8

No componentDidMountmétodo de ciclo de vida do componente , você pode definir um intervalo para chamar uma função que atualiza o estado.

 componentDidMount() {
      setInterval(() => this.setState({ time: Date.now()}), 1000)
 }
erik-sn
fonte
4
Correto, mas como a primeira resposta sugere, lembre-se de limpar o tempo para evitar vazamento de memória: componentWillUnmount () {clearInterval (this.interval); }
Alexander Falk
3
class ShowDateTime extends React.Component {
   constructor() {
      super();
      this.state = {
        curTime : null
      }
    }
    componentDidMount() {
      setInterval( () => {
        this.setState({
          curTime : new Date().toLocaleString()
        })
      },1000)
    }
   render() {
        return(
          <div>
            <h2>{this.state.curTime}</h2>
          </div>
        );
      }
    }
Jagadeesh
fonte
0

Então você estava no caminho certo. Dentro de seu, componentDidMount()você poderia ter concluído o trabalho implementando setInterval()para acionar a mudança, mas lembre-se de que a maneira de atualizar o estado de um componente é via setState(), portanto, dentro de seu componentDidMount()você poderia ter feito isso:

componentDidMount() {
  setInterval(() => {
   this.setState({time: Date.now()})    
  }, 1000)
}

Além disso, você usa o Date.now()que funciona, com a componentDidMount()implementação que ofereci acima, mas obterá um longo conjunto de atualizações de números desagradáveis ​​que não são legíveis por humanos, mas é tecnicamente o tempo de atualização a cada segundo em milissegundos desde 1 de janeiro de 1970, mas nós deseja tornar este tempo legível para como nós, humanos, lemos o tempo, portanto, além de aprender e implementar, setIntervalvocê deseja aprender new Date()e toLocaleTimeString()implementá-lo da seguinte forma:

class TimeComponent extends Component {
  state = { time: new Date().toLocaleTimeString() };
}

componentDidMount() {
  setInterval(() => {
   this.setState({ time: new Date().toLocaleTimeString() })    
  }, 1000)
}

Observe que também removi a constructor()função, você não precisa dela necessariamente, meu refator é 100% equivalente a inicializar o site com a constructor()função.

Daniel
fonte
0

Devido às mudanças no React V16, onde componentWillReceiveProps () foi preterido, esta é a metodologia que uso para atualizar um componente. Observe que o exemplo abaixo está em Typescript e usa o método getDerivedStateFromProps estático para obter o estado inicial e o estado atualizado sempre que os Props são atualizados.

    class SomeClass extends React.Component<Props, State> {
  static getDerivedStateFromProps(nextProps: Readonly<Props>): Partial<State> | null {
    return {
      time: nextProps.time
    };
  }

  timerInterval: any;

  componentDidMount() {
    this.timerInterval = setInterval(this.tick.bind(this), 1000);
  }

  tick() {
    this.setState({ time: this.props.time });
  }

  componentWillUnmount() {
    clearInterval(this.timerInterval);
  }

  render() {
    return <div>{this.state.time}</div>;
  }
}
Jarora
fonte