React Hooks的onchange事件对于扫描输入太慢了

4

我在使用下面的纯函数扫描条形码时遇到了问题。 我在useeffect中绑定了一个keydown事件,因为我需要查找陷阱代码。 当用户输入值时,它记录得很好,但是扫描仪似乎跳过了一两个字符。

import React, { useState, useEffect, useRef, useCallback } from 'react';
import * as propTypes from 'prop-types';
import styled from 'styled-components';

const ScannerInputWrapper = styled.input`
  visibility: hidden;
  display: none;
  width: 0%;
`;

// the current app context would be passed in as a prop to this function
function ScannerInputField({
  handleScannerInput,
  isScannerInputDisabled,
  isDisabled,
}) {
  const [dataEntered, setDataEntered] = useState('');
  const [nullarised, setNull] = useState('');
  const [scanStarted, setScanStarted] = useState(false);
  const inputRef = useRef(null);

  useEffect(() => {
    document.addEventListener('keydown', handleKeys);

    return () => {
      document.removeEventListener('keydown', handleKeys);
    };
  }, [dataEntered]);

  // focus on the text box at all points. Context awareness will need to come into this.
  useEffect(() => {
    inputRef.current.focus();
    inputRef.current.select();
  }, []);

  const handleKeys = useCallback(e => {
    e.preventDefault();

    if ((e.shiftKey || e.ctrlKey || e.altKey) && e.key.length > 1) {
      return;
    }

    if (e.key === '<') {
      setScanStarted(true);
      return;
    }

    if (e.key === '`') {
      // scan finished, lets send local data to higher function
      // handleScannerInput(dataEntered);
      console.log(dataEntered);
      setScanStarted(false);
      setDataEntered('');
      return;
    }

    if (e.key === 'Enter' && !scanStarted && dataEntered !== '') {
      // scan finished, lets send local data to higher function
      // handleScannerInput(dataEntered);
      console.log(dataEntered);
      setDataEntered('');
      return;
    }

    if (e.key.length === 1) {
      const code = e.keyCode ? e.keyCode : e.which;
      // having to do the below due to running an effect and reading from use state
      // causes you to read data incorrectly at high velocity
      const val = dataEntered.concat(String.fromCharCode(code));
      setDataEntered(val);
    }
  });

  return (
    <ScannerInputWrapper
      type="text"
      onChange={value => setNull(value)}
      value={dataEntered}
      disabled={isDisabled || isScannerInputDisabled}
      ref={inputRef}
      tabIndex={-1}
    />
  );
}

ScannerInputField.propTypes = {
  handleScannerInput: propTypes.func.isRequired,
  isScannerInputDisabled: propTypes.bool.isRequired,
  isDisabled: propTypes.bool.isRequired,
};

ScannerInputWrapper.whyDidYouRender = true;

export default ScannerInputField;

我知道不是每个人都有扫描仪,但如果有人发现我做错了什么蠢事,我会感激指导。

测试输出

输入图像描述

使用React v16.8.6


ScannerInputWrapper 在渲染上是否很耗费资源?每当用户输入时,由于 onChange={value => handleKeys(value)}(在每次按键时都会创建一个新的回调实例),它将重新渲染。您会尝试在其上使用 useCallback 吗?您可以在 React 开发工具中检查渲染频率、突出显示更新,并尝试 why-did-you-render - dance2die
谢谢,我尝试了你提到的内容,虽然我确实发现了 onchange 事件运行时的一个缓慢点,但我进行了一些修改。现在我会更新我的帖子并提供更多信息。 - Eric
经过进一步测试,我实际上认为这与渲染速度无关,而是useState钩子的返回值和set*状态函数没有互斥。虽然我还没有深入代码进行确认,但我已经添加了更多日志记录,并且可以看到在捕获一个事件时,需要两个事件才能更新状态值并添加一个额外字符。 - Eric
我已经尝试在函数中使用 Promise 来确保它不会批量处理渲染。但问题仍然存在。明显是 useState 实现的问题。 - Eric
1个回答

2

好的,useState太异步了。解决方案是在函数中使用状态对象内的reducer代替。效果非常好!

https://www.reddit.com/r/reactjs/comments/a3y76f/react_hooks_setstate_gotcha/

以下是修正后的代码:

/**
 *
 * ScannerInputField
 *
 */

import React, { useState, useEffect, useRef, useReducer } from 'react';
import * as propTypes from 'prop-types';
import styled from 'styled-components';

const ScannerInputWrapper = styled.input`
  visibility: hidden;
  display: none;
  width: 0%;
`;
const initialState = {
  barcodeInProgress: false,
  barcodeComplete: false,
  value: '',
};

const keyDownReducer = (state = initialState, e) => {
  if ((e.shiftKey || e.ctrlKey || e.altKey) && e.key.length > 1) {
    return state;
  }

  if (e.key === '<') {
    return { ...state, barcodeInProgress: true };
  }

  if (e.key === '`') {
    return { ...state, barcodeInProgress: false, barcodeComplete: true };
  }

  if (e.key === '_') {
    return {
      ...state,
      barcodeInProgress: false,
      barcodeComplete: false,
      value: '',
    };
  }

  if (e.key.length === 1) {
    return { ...state, value: state.value + e.key };
  }

  return state;
};

// the current app context would be passed in as a prop to this function
function ScannerInputField({
  handleScannerInput,
  isScannerInputDisabled,
  isDisabled,
}) {
  const inputRef = useRef(null);
  const [state, dispatch] = useReducer(keyDownReducer, initialState);
  const [nullarised, setNull] = useState('');

  useEffect(() => {
    if (state.barcodeComplete) {
      handleScannerInput(state.value);
      // startFromFresh
      dispatch(new KeyboardEvent('keypress', { key: '_' }));
    }
  }, [state.barcodeComplete]);

  useEffect(() => {
    document.addEventListener('keydown', handleKeysViaReducer);

    return () => document.removeEventListener('keydown', handleKeysViaReducer);
  }, [state]);

  // focus on the text box at all points. Context awareness will need to come into this.
  useEffect(() => {
    inputRef.current.focus();
    inputRef.current.select();
  }, []);

  const handleKeysViaReducer = e => {
    e.preventDefault();
    dispatch(e);
  };

  return (
    <ScannerInputWrapper
      type="text"
      onChange={value => setNull(value)}
      value={state.value}
      disabled={isDisabled || isScannerInputDisabled}
      ref={inputRef}
      tabIndex={-1}
    />
  );
}

ScannerInputField.propTypes = {
  handleScannerInput: propTypes.func.isRequired,
  isScannerInputDisabled: propTypes.bool.isRequired,
  isDisabled: propTypes.bool.isRequired,
};

ScannerInputWrapper.whyDidYouRender = false;

export default ScannerInputField;


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接