From 0d62370108a2c58de28726d7fa359421e6aff30f Mon Sep 17 00:00:00 2001 From: hectahertz Date: Thu, 26 Feb 2026 13:41:54 +0100 Subject: [PATCH 1/2] perf(TextInput): skip redundant character counter updates --- packages/react/src/TextInput/TextInput.tsx | 40 +++++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) 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) }, From f688fd5541b0d6382c597c85b9c075fa384eaab6 Mon Sep 17 00:00:00 2001 From: hectahertz Date: Thu, 26 Feb 2026 13:43:20 +0100 Subject: [PATCH 2/2] chore: changeset --- .changeset/perf-textinput-character-counter-updates.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changeset/perf-textinput-character-counter-updates.md 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