React会话超时问题排查:弹窗计时不准与立即登出异常
会话超时功能问题排查与修复
功能需求
- 会话将在5分钟后超时
- 超时前2分钟弹出警告弹窗
- 点击Yes续期会话,弹窗关闭后重新开始5分钟计时
- 点击No关闭弹窗,会话需在2分钟后超时
现存问题
- 点击No时立即跳转至会话过期页面,无法实现2分钟后过期
- 警告弹窗上的计时器不准确,有时提前30秒、40秒或2秒结束
原代码
import React, { useEffect} from "react"; import { Modal } from 'react-bootstrap'; export const SessionTimeoutWarningModal = ({ isOpen, onRequestClose }) => { const onLogOffCall = () => { // Implement your logout functionality here window.location.href = "/SessionExpired"; } const [counter, setCounter] = React.useState(120); useEffect(() => { const timer = counter > 0 && setInterval(() => setCounter(counter - 1), 1000); return () => clearInterval(timer); }, [counter]); return ( <div> <Modal show={isOpen} className="modalPopup" contentLabel="Session Timeout"> <div className="modalPopupHeader"> Session Expiring! </div> <div className="modalPopupBody"> Your session will expire in <span id="seconds">{counter}</span> seconds.<br /> Do you want to reset session timeout? </div> <div className="modalPopupFooter" align="center"> <button onClick={onRequestClose} className="Savebutton">Yes</button> <button onClick={onLogOffCall} className="ButtonCancel">No</button> </div> </Modal> </div> ); } import React, { useEffect, useState } from "react"; import { SessionTimeoutWarningModal } from "./SessionTimeoutWarningModal"; const timeout_ms = 5 * 60 * 1000; //5 minutes export const SessionTimeout = () => { const [isWarningModalOpen, setWarningModalOpen] = useState(false); useEffect(() => { const createTimeout1 = () => setTimeout(() => { setWarningModalOpen(true); }, timeout_ms - 120 * 1000) //Show Model Dialog 2 minutes before timeout (at 3 minutes) const createTimeout2 = () => setTimeout(() => { // Implement a sign out function here window.location.href = "/AdminSessionExpired"; }, 120*1000) const listener = () => { if (!isWarningModalOpen) { clearTimeout(timeout) timeout = createTimeout1(); } } // Initialization let timeout = isWarningModalOpen ? createTimeout2() : createTimeout1() // Cleanup return () => { clearTimeout(timeout); } }, [isWarningModalOpen]) return ( <div> { isWarningModalOpen && ( <SessionTimeoutWarningModal isOpen={isWarningModalOpen} onRequestClose={() => setWarningModalOpen(false)} /> ) } </div> ) } export default SessionTimeout; import React from "react"; import {Link, Redirect} from 'react-router-dom'; export class SessionExpired extends React.Component { constructor(props) { super(props); this.state = { navigate: false }; this.onSessionExpired = this.onSessionExpired.bind(this); } onSessionExpired(event) { localStorage.clear(); window.sessionStorage.clear(); this.setState({ navigate: true }); } render() { const { navigate } = this.state; if (navigate) { return <Redirect to="/" push={true} />; } return ( <div> <div id="container"> <br /> <h5><b>Session Expired</b></h5> <label>Your session has expired. Please <Link className="HomePageLink" onClick={this.onSessionExpired} > return to home!</Link></label> </div> </div> ); } } export default SessionExpired;
问题分析与修复方案
问题1:点击No立即跳转的原因
- 弹窗组件
SessionTimeoutWarningModal的onLogOffCall直接执行页面跳转,未触发父组件的2分钟超时逻辑 - 父组件
SessionTimeout的超时管理未与弹窗No按钮联动,点击No后未启动120秒倒计时跳转
修复步骤:
- 弹窗新增
onNoClick属性,将No按钮点击事件传递给父组件处理 - 父组件点击No后,标记进入最终倒计时状态,启动120秒超时跳转
问题2:计时器不准确的原因
- 使用
setCounter(counter - 1)存在闭包问题:interval触发时捕获的是创建时的counter值,而非最新状态 - 弹窗关闭后重新打开,
counter未重置为120,导致从上次剩余数值开始计时 - 每次
counter变化都创建新interval,虽有清理但仍可能出现计时偏差
修复步骤:
- 改用函数式更新
setCounter(prev => prev - 1),确保取最新状态值 - 监听弹窗
isOpen状态,打开时重置counter为120 - 优化interval创建逻辑,避免重复创建
修复后的代码
SessionTimeoutWarningModal.js
import React, { useEffect, useState } from "react"; import { Modal } from 'react-bootstrap'; export const SessionTimeoutWarningModal = ({ isOpen, onRequestClose, onNoClick }) => { const [counter, setCounter] = useState(120); // 弹窗打开时重置计时器,关闭时清理interval useEffect(() => { if (!isOpen) return; setCounter(120); const timer = setInterval(() => { setCounter(prev => { if (prev <= 1) { clearInterval(timer); onNoClick(); // 计时结束自动触发No逻辑 return 0; } return prev - 1; }); }, 1000); return () => clearInterval(timer); }, [isOpen, onNoClick]); return ( <div> <Modal show={isOpen} className="modalPopup" contentLabel="Session Timeout"> <div className="modalPopupHeader"> Session Expiring! </div> <div className="modalPopupBody"> Your session will expire in <span id="seconds">{counter}</span> seconds.<br /> Do you want to reset session timeout? </div> <div className="modalPopupFooter" align="center"> <button onClick={onRequestClose} className="Savebutton">Yes</button> <button onClick={onNoClick} className="ButtonCancel">No</button> </div> </Modal> </div> ); }
SessionTimeout.js
import React, { useEffect, useState } from "react"; import { SessionTimeoutWarningModal } from "./SessionTimeoutWarningModal"; const TIMEOUT_MS = 5 * 60 * 1000; // 5分钟 const WARNING_BEFORE_MS = 2 * 60 * 1000; // 超时前2分钟弹窗 const FINAL_COUNTDOWN_MS = 2 * 60 * 1000; // 点击No后等待2分钟超时 export const SessionTimeout = () => { const [isWarningModalOpen, setWarningModalOpen] = useState(false); const [isFinalCountdown, setIsFinalCountdown] = useState(false); let timeoutId = null; // 重置会话计时 const resetSessionTimeout = () => { if (timeoutId) clearTimeout(timeoutId); setIsFinalCountdown(false); setWarningModalOpen(false); // 设置5分钟后触发弹窗的计时器 timeoutId = setTimeout(() => { setWarningModalOpen(true); }, TIMEOUT_MS - WARNING_BEFORE_MS); }; // 处理点击No的逻辑:启动最终2分钟倒计时 const handleNoClick = () => { setWarningModalOpen(false); setIsFinalCountdown(true); if (timeoutId) clearTimeout(timeoutId); // 2分钟后跳转至过期页面 timeoutId = setTimeout(() => { window.location.href = "/AdminSessionExpired"; }, FINAL_COUNTDOWN_MS); }; // 监听用户活动,重置会话计时 useEffect(() => { const handleUserActivity = () => { if (!isFinalCountdown) { resetSessionTimeout(); } }; // 绑定用户活动事件 window.addEventListener('mousemove', handleUserActivity); window.addEventListener('keydown', handleUserActivity); window.addEventListener('click', handleUserActivity); // 初始化计时器 resetSessionTimeout(); // 清理函数 return () => { window.removeEventListener('mousemove', handleUserActivity); window.removeEventListener('keydown', handleUserActivity); window.removeEventListener('click', handleUserActivity); if (timeoutId) clearTimeout(timeoutId); }; }, [isFinalCountdown]); return ( <div> {isWarningModalOpen && ( <SessionTimeoutWarningModal isOpen={isWarningModalOpen} onRequestClose={resetSessionTimeout} onNoClick={handleNoClick} /> )} </div> ); } export default SessionTimeout;
SessionExpired.js(无需修改)
import React from "react"; import { Link, Redirect } from 'react-router-dom'; export class SessionExpired extends React.Component { constructor(props) { super(props); this.state = { navigate: false }; this.onSessionExpired = this.onSessionExpired.bind(this); } onSessionExpired(event) { localStorage.clear(); window.sessionStorage.clear(); this.setState({ navigate: true }); } render() { const { navigate } = this.state; if (navigate) { return <Redirect to="/" push={true} />; } return ( <div> <div id="container"> <br /> <h5><b>Session Expired</b></h5> <label>Your session has expired. Please <Link className="HomePageLink" onClick={this.onSessionExpired} > return to home!</Link></label> </div> </div> ); } } export default SessionExpired;
关键优化点
- 将超时逻辑统一放在父组件
SessionTimeout中管理,弹窗仅负责UI交互 - 使用函数式状态更新解决计时器闭包问题,确保计时准确
- 弹窗打开时自动重置计时器,避免残留数值
- 绑定用户活动事件(鼠标移动、键盘输入、点击),实现会话续期功能
- 点击No后启动独立的2分钟倒计时,确保不会立即跳转
内容的提问来源于stack exchange,提问作者TestUser
相关产品推荐
相关产品推荐

