diff --git a/.changeset/perf-textinput-character-counter-updates.md b/.changeset/perf-textinput-character-counter-updates.md new file mode 100644 index 00000000000..fd4e071282b --- /dev/null +++ b/.changeset/perf-textinput-character-counter-updates.md @@ -0,0 +1,4 @@ +--- +'@primer/react': patch +--- +perf(TextInput): skip redundant character counter updates diff --git a/packages/react/src/TextInput/TextInput.tsx b/packages/react/src/TextInput/TextInput.tsx index 6f30c31428e..88a4c716226 100644 --- a/packages/react/src/TextInput/TextInput.tsx +++ b/packages/react/src/TextInput/TextInput.tsx @@ -108,6 +108,10 @@ const TextInput = React.forwardRef( const [isOverLimit, setIsOverLimit] = useState(false) const [screenReaderMessage, setScreenReaderMessage] = useState('') const characterCounterRef = useRef(null) + const lastCountedLengthRef = useRef(null) + const lastCharacterCountRef = useRef('') + const lastIsOverLimitRef = useRef(false) + const lastScreenReaderMessageRef = useRef('') // this class is necessary to style FilterSearch, plz no touchy! const wrapperClasses = clsx(className, 'TextInput-wrapper') @@ -157,17 +161,33 @@ const TextInput = React.forwardRef( if (characterLimit) { characterCounterRef.current = new CharacterCounter({ onCountUpdate: (count, overLimit, message) => { - setCharacterCount(message) - setIsOverLimit(overLimit) + if (message !== lastCharacterCountRef.current) { + lastCharacterCountRef.current = message + setCharacterCount(message) + } + + if (overLimit !== lastIsOverLimitRef.current) { + lastIsOverLimitRef.current = overLimit + setIsOverLimit(overLimit) + } }, onScreenReaderAnnounce: message => { - setScreenReaderMessage(message) + if (message !== lastScreenReaderMessageRef.current) { + lastScreenReaderMessageRef.current = message + setScreenReaderMessage(message) + } }, }) + lastCountedLengthRef.current = null + return () => { characterCounterRef.current?.cleanup() characterCounterRef.current = null + lastCountedLengthRef.current = null + lastCharacterCountRef.current = '' + lastIsOverLimitRef.current = false + lastScreenReaderMessageRef.current = '' } } }, [characterLimit]) @@ -177,7 +197,12 @@ const TextInput = React.forwardRef( if (characterLimit && characterCounterRef.current) { const currentValue = value !== undefined ? String(value) : defaultValue !== undefined ? String(defaultValue) : '' - characterCounterRef.current.updateCharacterCount(currentValue.length, characterLimit) + const currentLength = currentValue.length + + if (currentLength !== lastCountedLengthRef.current) { + lastCountedLengthRef.current = currentLength + characterCounterRef.current.updateCharacterCount(currentLength, characterLimit) + } } }, [value, defaultValue, characterLimit]) @@ -185,7 +210,12 @@ const TextInput = React.forwardRef( const handleInputChange = useCallback( (e: React.ChangeEvent) => { if (characterLimit && characterCounterRef.current) { - characterCounterRef.current.updateCharacterCount(e.target.value.length, characterLimit) + const currentLength = e.target.value.length + + if (currentLength !== lastCountedLengthRef.current) { + lastCountedLengthRef.current = currentLength + characterCounterRef.current.updateCharacterCount(currentLength, characterLimit) + } } onChange?.(e) },