Почему currentVal присваивает последний введенный символ с клавиатуры?

Решил переписать классовый компонент калькулятора на функциональный.

Столкнулся с тем, что при попытке ввода числа с клавиатуры записывается и отображается только последний символ.

Хотел бы узнать, что я сделал не так.

Код компонента прикладываю ниже:

import React, {useState, useEffect, useCallback} from 'react';
import styles from './Calculator.module.css';

import Display from '../../UI/Display/Display';
import Button from '../../UI/Button/Button';

const Calculator = (props) => {
    const [operation, setOperation] = useState(null);
    const [currentVal, setCurrentVal] = useState(0);
    const [prevVal, setPrevVal] = useState(0);
    const btnsVal = [
            'C', '√', 'x²', '/',
            '7', '8', '9', '*',
            '4', '5', '6', '-',
            '1', '2', '3', '+',
            '.', '0', 'Del', '='
    ];

    const btnsArr = btnsVal.map((val, index) =>
        <Button
            key={`${index}-${val}`}
            value={val}
            onClick={() => clickEventHandler(val)}
        />
    );

    const calcOperations = {
        '/': (prevValue, currentValue) => prevValue / currentValue,
        '*': (prevValue, currentValue) => prevValue * currentValue,
        '+': (prevValue, currentValue) => Number(prevValue) + Number(currentValue),
        '-': (prevValue, currentValue) => prevValue - currentValue,
        '=': currentValue => currentValue,
        '√': currentValue => Math.sqrt(currentValue),
        'x²': currentValue => currentValue**2
    };

    useEffect(() => {
        document.addEventListener('keydown', keydownEventHandler);
        return () => document.removeEventListener('keydown', keydownEventHandler);
    }, []);

    const keydownEventHandler = useCallback(({key}) => {
        if (btnsVal.includes(key) ){
            clickEventHandler(key);
        }else if (key === 'Backspace'){
            clickEventHandler('Del');
        }else if (key === ','){
            clickEventHandler('.');
        }else if (key === 'Enter'){
            clickEventHandler('=');
        }
    }, [btnsVal]);

    useEffect(() => {
        !currentVal && setCurrentVal('0');
        !prevVal && setPrevVal('0');
    }, [currentVal, prevVal]);

    const writeNum = (symbol) => {
        const value = (symbol === '.' || currentVal.toString() !== '0')
            ? currentVal  + symbol
            : symbol;
        if (Array.from(value).filter(item => item === '.').length < 2){
            setCurrentVal(value);
        }
    }

    const choiceOperation = (symbol) => {
        if (['√','x²'].includes(symbol)){
            const currVal = calcOperations[symbol](Number(currentVal) || prevVal);
            operation === '=' && setOperation(null);
            setCurrentVal(currVal);
            return;
        } else if (operation){
            setPrevVal(calcOperations[operation](prevVal, currentVal));
        }else {
            setPrevVal(currentVal);
        }
        setCurrentVal(0);
        setOperation(symbol);
    }

    const clickEventHandler = (symbol) => {
        if (/(\d|\.)/.test(symbol)) {
            writeNum(symbol);
        }else if (symbol === 'Del') {
            setCurrentVal(`${currentVal}`.slice(0, -1));
        }else if (symbol === 'C') {
            setOperation(null);
            setCurrentVal(0);
            setPrevVal(0);
        }else{
            choiceOperation(symbol);
        }
    }

    return (
        <div className={styles.Calculator}>
            <Display result={operation === '=' ? prevVal : currentVal}/>
            <div className={styles.btnsBlock}>{btnsArr}</div>
        </div>
    );
}

export default Calculator;

Ответы (1 шт):

Автор решения: Sergei Kirjanov

(1) Лучше использовать форму set*, принимающую функцию с прошлым значением на входе.

Конкретно currentVal использовать при рендере, а не в обработчиках.

(2) Прописывать все зависимости у useEffect/useCallback. Изнутри useEffect/useCallback можно обращаться только к описанным зависимостям, или к неизменяемым за время жизни компонента (set*).

Видимо, например, useEffect с addEventListener должен зависить от keydownEventHandler.

(3) Сейчас btnsVal пересоздается при каждом рендере и сбрасывает keydownEventHandler. Чтобы зависимость от btnsVal работала, он должен быть рядом с компонентом (или useMemo).

→ Ссылка