Next.js计算器应用:键盘输入触发数字状态重置问题排查
问题描述
用Next.js开发简易计算器,HTML按钮操作完全正常,但添加键盘输入的事件监听器后,按下指定按键时数字状态会先重置为0,再变为对应按键的数字(预期是追加到现有数字后)。排查发现调用appendNumber函数前number状态就已变为0,其他状态不受影响,启用React严格模式也未解决问题。
代码示例
'use client' import styles from './page.module.css' import React, {useState, useEffect, useRef} from "react"; import Image from "next/image"; export default function Home() { const [number, setNumber] = useState('0'); const [number2, setNumber2] = useState(''); const [lastOperator, setLastOperator] = useState(null); const [clear, setClear] = useState('AC'); const [isOperational, setIsOperational] = useState(false); const numberRef = useRef(); const number2Ref = useRef(); useEffect(() => { document.addEventListener("keydown", handleKeyPress); // Clean up the event listener when the component unmounts. return () => { document.removeEventListener('keydown', handleKeyPress); }; }, []) function handleKeyPress(event) { console.log('number: ' + number) const keyMap = { '1': () => appendNumber('1'), '2': () => appendNumber('2'), '3': () => appendNumber('3'), '4': () => appendNumber('4'), '5': () => appendNumber('5'), '6': () => appendNumber('6'), '7': () => appendNumber('7'), '8': () => appendNumber('8'), '9': () => appendNumber('9'), '0': () => appendNumber('0'), 'Enter': () => pressEquals(), }; const key = event.key; if (key in keyMap) { event.preventDefault(); // Prevent the default browser behavior for the key. keyMap[key](); } } function adjustFontSize() { const numberElement = numberRef.current; const numberWidth = numberElement.scrollWidth; const containerWidth = numberElement.parentElement.scrollWidth; if (numberWidth > (containerWidth) - containerWidth * 0.08) { const newFontSize = (containerWidth * 0.92 / numberWidth) * parseFloat(getComputedStyle(numberElement).fontSize); numberElement.style.fontSize = `${newFontSize}px`; if (isOperational) { number2Ref.current.style.fontSize = `${newFontSize}px`; } } else { numberElement.style.fontSize = '5rem'; if (isOperational) { number2Ref.current.style.fontSize = '3rem'; } } } function appendNumber(digit) { if (number !== '0') { if (isOperational) { setNumber2(number); setNumber(digit); } else { setNumber(number + digit); } } else { setNumber(digit); setClear('C'); } adjustFontSize(numberRef); if (isOperational) { setIsOperational(false); } } function pressC() { setNumber('0'); setNumber2('0'); setIsOperational(false); setLastOperator(null); setClear('AC'); adjustFontSize(); } function pressNeg() { if (number.startsWith('-')) { setNumber(number.slice(1)); } else if (number !== '0') { setNumber('-' + number); } adjustFontSize(); } function pressPoint() { if (!number.includes('.')) { setNumber(number + '.'); } adjustFontSize(); } function pressOperator(operator) { setLastOperator(operator); if (number2 !== '0') { pressEquals(); setIsOperational(true); } else { setNumber2(number); setNumber('0'); } } function pressBackspace() { setNumber(number.slice(0, number.length - 1)) adjustFontSize(); } function pressEquals() { const isFloatingPoint = number.includes('.') || number2.includes('.'); const num1 = isFloatingPoint ? parseFloat(number) : parseInt(number); const num2 = isFloatingPoint ? parseFloat(number2) : parseInt(number2); switch (lastOperator) { case '+': setNumber((num2 + num1).toString()); break; case '-': setNumber((num2 - num1).toString()); break; case '*': setNumber((num2 * num1).toString()); break; case '/': setNumber((num2 / num1).toString()); break; case '^': setNumber(Math.pow(num2, num1).toString()); break; } setNumber2('0'); } return ( <main className={styles.main}> <div id={styles.calculator}> <div id={styles.display}> <div id={styles.number2Div}> <p id={styles.number2} ref={number2Ref}>{number2}</p> </div> <div id={styles.numberDiv}> <p id={styles.number} ref={numberRef}>{number}</p> </div> </div> <div id={styles.buttons}> <button onClick={pressC} className={styles.button}>{clear}</button> <button onClick={pressNeg} className={styles.button}>neg</button> <button onClick={() => pressOperator('^')} className={styles.button}>^</button> <button onClick={() => pressOperator('/')} className={styles.button}>/</button> <button onClick={() => appendNumber('1')} className={styles.button}>1</button> <button onClick={() => appendNumber('2')} className={styles.button}>2</button> <button onClick={() => appendNumber('3')} className={styles.button}>3</button> <button onClick={() => pressOperator('*')} className={styles.button}>*</button> <button onClick={() => appendNumber('4')} className={styles.button}>4</button> <button onClick={() => appendNumber('5')} className={styles.button}>5</button> <button onClick={() => appendNumber('6')} className={styles.button}>6</button> <button onClick={() => pressOperator('-')} className={styles.button}>-</button> <button onClick={() => appendNumber('7')} className={styles.button}>7</button> <button onClick={() => appendNumber('8')} className={styles.button}>8</button> <button onClick={() => appendNumber('9')} className={styles.button}>9</button> <button onClick={() => pressOperator('+')} className={styles.button}>+</button> <button onClick={pressBackspace} className={styles.button}><Image width={64} height={64} src="/backspace.png" alt="Back"/> </button> <button onClick={() => appendNumber('0')} className={styles.button}>0</button> <button onClick={pressPoint} className={styles.button}>.</button> <button onClick={pressEquals} className={styles.button}>=</button> </div> </div> </main> ) }
问题原因
核心问题是闭包导致的状态捕获:
useEffect的依赖数组为空,意味着handleKeyPress函数只会在组件初始挂载时创建一次,它内部引用的number、isOperational等状态都是初始值(number初始为'0')。- 后续状态更新后,
handleKeyPress并不会重新创建,依然持有旧的状态引用,所以当键盘触发appendNumber时,函数里判断的number始终是初始的'0',导致执行else分支直接设置number = digit,而不是追加。
按钮操作正常是因为按钮的onClick回调会在每次渲染时重新创建,始终引用最新的状态。
修复方案
方案一:使用函数式更新(推荐)
修改appendNumber函数,使用setNumber的函数式更新形式,直接基于前一个状态计算新值,避免依赖当前的状态变量:
function appendNumber(digit) { setNumber(prevNumber => { let newNumber = prevNumber; if (prevNumber !== '0') { if (isOperational) { setNumber2(prevNumber); newNumber = digit; } else { newNumber = prevNumber + digit; } } else { newNumber = digit; } return newNumber; }); if (number === '0') { setClear('C'); } adjustFontSize(numberRef); if (isOperational) { setIsOperational(false); } }
方案二:更新useEffect的依赖数组
让useEffect在相关状态变化时重新绑定事件监听器,确保handleKeyPress始终引用最新的状态:
useEffect(() => { document.addEventListener("keydown", handleKeyPress); return () => { document.removeEventListener('keydown', handleKeyPress); }; }, [number, isOperational, clear]) // 添加所有在handleKeyPress和appendNumber中用到的状态依赖
同时可以把handleKeyPress用useCallback包裹,避免不必要的重新创建:
const handleKeyPress = useCallback((event) => { console.log('number: ' + number) const keyMap = { '1': () => appendNumber('1'), '2': () => appendNumber('2'), '3': () => appendNumber('3'), '4': () => appendNumber('4'), '5': () => appendNumber('5'), '6': () => appendNumber('6'), '7': () => appendNumber('7'), '8': () => appendNumber('8'), '9': () => appendNumber('9'), '0': () => appendNumber('0'), 'Enter': () => pressEquals(), }; const key = event.key; if (key in keyMap) { event.preventDefault(); keyMap[key](); } }, [number, appendNumber, pressEquals])
方案一更简洁,不需要频繁绑定事件监听器,优先推荐。
内容的提问来源于stack exchange,提问作者delsix
相关产品推荐
相关产品推荐

