You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 00:40:55