React中Main组件定时更新数据导致Modal计时器异常的问题排查
Fixing Your React Timer Glitch: Why the Modal Timer Speeds Up and Hits -1
Hey Vlad, let's dig into why your timers are misbehaving and get them working right. I spot a couple of key issues in both your Main and Modal components that are causing the chaos.
What's Going Wrong?
1. Main Component: Misusing Lifecycle Methods & Leaking Timers
- You're manually calling
componentWillMount()in yoursetInterval—this is a big no-no. Lifecycle methods are meant to be called by React, not your code. Every time you runcomponentWillMount(), it triggers asetState, which re-renders theMaincomponent. This re-render triggerscomponentDidUpdatein theModaleven if theisOpenedprop hasn't changed. - Your
componentWillUnmountonly setsthis.timer = null—it doesn't actually clear the interval withclearInterval(). This leaves the interval running in the background, causing endless re-renders and memory leaks.
2. Modal Component: Overlapping Timers & Stale State
- In
componentDidUpdate, you're creating a newsetTimeoutevery time the component updates (including whenMainre-renders due to its interval). This means multiple timers are running at the same time, making the counter tick down faster than 1 second per step. - You're using
this.state.counterdirectly insetState, which can lead to stale state values (sincesetStateis asynchronous). When multiple timers fire, they might all use the same old counter value, leading to unexpected drops like going from 1 to -1. - You don't clear the timer when the counter hits 0, so the timer keeps running and tries to decrement a counter that's already at 0.
The Fixes
Step 1: Fix the Main Component's Data Fetching & Timer
First, let's clean up the Main component by separating data fetching into a dedicated method, fixing the interval cleanup, and ditching the manual lifecycle method call:
import React, { Component } from 'react'; class Main extends Component { constructor(props) { super(props); this.state = { data: [], isOpened: false }; this.submitForm = this.submitForm.bind(this); this.fetchData = this.fetchData.bind(this); // Bind the fetch method } // Separate data fetching into its own method fetchData() { fetch('/api/global.json') .then(response => response.json()) .then(result => this.setState({ data: result.data })); } componentDidMount() { this.fetchData(); // Initial fetch this.interval = setInterval(this.fetchData, 10000); // Call the dedicated method, not a lifecycle hook } componentWillUnmount() { clearInterval(this.interval); // Actually clear the interval to prevent leaks } changeModal = () => { this.setState({ isOpened: !this.state.isOpened }); } submitForm(e) { e.preventDefault(); this.setState({ isOpened: !this.state.isOpened }); } render() { return ( <div> <form onSubmit={this.submitForm}> <button className="button button-main" type="submit">Modal open</button> </form> <Modal isOpened={this.state.isOpened} changeModal={this.changeModal} /> </div> ); } }
Step 2: Fix the Modal Component's Timer Logic
Now let's fix the Modal timer to prevent overlapping timers, use safe state updates, and clean up properly:
class Modal extends Component { constructor(props) { super(props); this.state = { counter: 30 }; this.timer = null; } componentDidUpdate(prevProps) { // Only handle timer logic when isOpened changes if (prevProps.isOpened !== this.props.isOpened) { if (this.props.isOpened) { // Start timer when modal opens this.startTimer(); } else { // Reset and clear timer when modal closes clearTimeout(this.timer); this.setState({ counter: 30 }); } } } startTimer() { // Clear any existing timer first to avoid overlaps if (this.timer) clearTimeout(this.timer); if (this.state.counter > 0) { this.timer = setTimeout(() => { // Use functional setState to get the latest state value this.setState(prevState => ({ counter: prevState.counter - 1 }), () => { // Recursively start the next timer only if counter is still > 0 if (this.state.counter > 0) { this.startTimer(); } }); }, 1000); } } componentWillUnmount() { clearTimeout(this.timer); // Clean up timer when component unmounts } render() { const padTime = time => { return String(time).length === 1 ? `0${time}` : `${time}`; }; const format = time => { const minutes = Math.floor(time / 60); const seconds = time % 60; return `${minutes} мин ${padTime(seconds)} сек`; }; return ( <div className={"modal-overlay" + (this.props.isOpened ? " open" : " close")}> <div className='modal-content'> <div className='modal-close' onClick={this.props.changeModal}>×</div> <div className="modal-note"> {this.state.counter === 0 ? 'Finish!' : `Timer ${format(this.state.counter)}`} </div> </div> </div> ) } }
Key Improvements Explained
- No manual lifecycle calls: We use a dedicated
fetchDatamethod instead of callingcomponentWillMountdirectly, preventing unnecessary re-renders. - Proper timer cleanup: Both components clear their timers in
componentWillUnmountto avoid memory leaks and leftover timers. - Functional setState: Using
prevStateensures we always have the latest counter value, preventing stale state bugs. - Controlled timer flow: The modal's timer is only started when the modal opens, and we clear existing timers before starting new ones to avoid overlaps.
- Stop timer at 0: The timer stops automatically once the counter reaches 0, so it won't decrement to -1.
内容的提问来源于stack exchange,提问作者Vlad
相关产品推荐
相关产品推荐

