Uh oh!
There was an error while loading. Please reload this page.
perf(TextInput): skip redundant character counter updates - #7600
Conversation
🦋 Changeset detectedLatest commit: dee4f87 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
There was a problem hiding this comment.
Pull request overview
Optimizes @primer/react’s TextInput character counter behavior to avoid redundant state updates/renders in controlled inputs with characterLimit, reducing wasted React work per keystroke.
Changes:
- Added refs to track last-counted input length and last-emitted counter/over-limit/aria-live messages to skip redundant updates.
- Guarded
CharacterCounter.updateCharacterCountcalls in both thevalueeffect andonChangehandler when length is unchanged. - Added a changeset for a patch release.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/react/src/TextInput/TextInput.tsx | Adds ref-based guards to avoid redundant character counter updates and state churn. |
| .changeset/perf-textinput-character-counter-updates.md | Declares a patch changeset entry for the performance fix. |
Comments suppressed due to low confidence (1)
packages/react/src/TextInput/TextInput.tsx:223
- The new
lastCountedLengthRefguard is primarily intended to avoid redundant counter updates in controlled TextInput usage, but the existing test suite only exercises the character counter in uncontrolled mode. Please add a controlled-input test (e.g. wrapper component withvalue+onChangeanduser.type) that asserts the counter/error state still updates correctly, and ideally thatCharacterCounter.updateCharacterCountis not invoked twice per keystroke (can be done by spying/mocking the class).
// Update character count when value changes or on mount
useEffect(() => {
if (characterLimit && characterCounterRef.current) {
const currentValue =
value !== undefined ? String(value) : defaultValue !== undefined ? String(defaultValue) : ''
const currentLength = currentValue.length
if (currentLength !== lastCountedLengthRef.current) {
lastCountedLengthRef.current = currentLength
characterCounterRef.current.updateCharacterCount(currentLength, characterLimit)
}
}
}, [value, defaultValue, characterLimit])
// Handle input change with character counter
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (characterLimit && characterCounterRef.current) {
const currentLength = e.target.value.length
if (currentLength !== lastCountedLengthRef.current) {
lastCountedLengthRef.current = currentLength
characterCounterRef.current.updateCharacterCount(currentLength, characterLimit)
}
}
onChange?.(e)
},
[onChange, characterLimit],
)
👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/14862 |
fbd4a1e to
ca369eaComparecef30ff to
77be5b9CompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Closes #
In a controlled
TextInputwithcharacterLimit, every keystroke was triggering 2 React render invocations instead of 1:handleInputChangecallsupdateCharacterCount(newLen)→setCharacterCount(msg)+setIsOverLimit(flag), batched with the parent'ssetValue→ one render + DOM commit.valueuseEffectfires next (becausevaluechanged), callsupdateCharacterCount(newLen)a second time →setCharacterCount(sameMsg)+setIsOverLimit(sameFlag). React 18 runs the full component function and reconciler to determine nothing changed, then discards the result without committing.The fix: track the last-counted length in a
lastCountedLengthRef. BothhandleInputChangeand thevalueeffect guard theirupdateCharacterCountcall behindcurrentLength !== lastCountedLengthRef.current. TheonChangehandler always fires first, so the effect always hits the guard and skips.setCharacterCountandsetIsOverLimitare also guarded insideonCountUpdateandonScreenReaderAnnouncewith value-equality checks.Performance measurements
Chrome DevTools MCP, 6x CPU throttle, React 18 dev build (
createRoot),characterLimit=20, controlled input (100 keystrokes).TextInputwithcharacterLimitChangelog
Changed
TextInputcharacter-counter update churn by skipping redundant count/state updates when the input length has not changed.aria-livemessage state updates when the announcement message is unchanged.Rollout strategy
Testing & Reviewing
runTestsforpackages/react/src/TextInput/TextInput.test.tsxand confirm all tests pass.Merge checklist