fix(ToggleSwitch): fire onChange from the interaction instead of an effect#8024
fix(ToggleSwitch): fire onChange from the interaction instead of an effect#8024mattcosta7 wants to merge 3 commits into
Conversation
…ffect onChange was called from a useEffect keyed on [onChange, checked, ...], so it fired on mount, whenever a controlled checked value changed externally, and on every render when onChange was an inline function. Call it from the click handler instead. Adds a regression test.
|
🦋 Changeset detectedLatest commit: cc87dff 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 |
|
Integration test results from github/github-ui PR: |
There was a problem hiding this comment.
Pull request overview
This PR fixes ToggleSwitch controlled-mode behavior so onChange is fired from the actual user interaction instead of a useEffect, preventing unintended calls on mount, on externally-driven checked updates, and on rerenders where onChange changes identity.
Changes:
- Move controlled
onChangeinvocation into the click handler and remove the effect that previously echoedcheckedchanges. - Add a regression test ensuring
onChangeis not called on mount or whencheckedchanges externally. - Add a patch changeset documenting the behavior fix.
Show a summary per file
| File | Description |
|---|---|
| packages/react/src/ToggleSwitch/ToggleSwitch.tsx | Removes effect-driven onChange and triggers onChange from the interaction handler for controlled usage. |
| packages/react/src/ToggleSwitch/ToggleSwitch.test.tsx | Adds regression coverage for unintended onChange calls in controlled mode. |
| .changeset/toggleswitch-onchange-from-handler.md | Documents the patch-level behavioral fix for consumers. |
Copilot's findings
- Files reviewed: 3/3 changed files
- Comments generated: 2
| // For controlled usage the click is the source of the change, so notify the | ||
| // consumer here rather than echoing the `checked` prop back from an effect | ||
| // (which fired on mount and whenever `checked` changed). |
| it('does not call onChange on mount or when checked changes externally', () => { | ||
| const handleChange = vi.fn() | ||
| const {rerender} = render( | ||
| <> | ||
| <div id="switchLabel">{SWITCH_LABEL_TEXT}</div> | ||
| <ToggleSwitch checked={false} onChange={handleChange} aria-labelledby="switchLabel" /> | ||
| </>, | ||
| ) | ||
| expect(handleChange).not.toHaveBeenCalled() | ||
|
|
||
| rerender( | ||
| <> | ||
| <div id="switchLabel">{SWITCH_LABEL_TEXT}</div> | ||
| <ToggleSwitch checked={true} onChange={handleChange} aria-labelledby="switchLabel" /> | ||
| </>, | ||
| ) | ||
| expect(handleChange).not.toHaveBeenCalled() | ||
| }) |
Overview
For a controlled
ToggleSwitch,onChangewas fired from an effect rather than from the user interaction:Because the effect is keyed on
onChangeandchecked, this causedonChangeto fire:checked(echoing a change the parent already made), andonChangeis an inline function (a new identity each render re-runs the effect).This PR moves
onChangeto the interaction handler, where a controlled input should call it, and deletes the effect:Changelog
Fixed
ToggleSwitch:onChangeno longer fires on mount, on externalcheckedchanges, or on every render with an inline handler. It now fires on user interaction, as expected for a controlled input.Rollout strategy
Testing & Reviewing
onChangetest still passes (the click still reports the new value).onChangedoes not fire on mount or on an externalcheckedchange.ToggleSwitchsuite (17 tests) passes.Merge checklist