Skip to content

Fix #416 : Checkbox isSelected state toggles internally#501

Open
viraj0075 wants to merge 1 commit into
instacart:masterfrom
viraj0075:fix-checkbox-controlled-state
Open

Fix #416 : Checkbox isSelected state toggles internally#501
viraj0075 wants to merge 1 commit into
instacart:masterfrom
viraj0075:fix-checkbox-controlled-state

Conversation

@viraj0075

@viraj0075 viraj0075 commented May 25, 2026

Copy link
Copy Markdown

Fixes #416.

This PR resolves an issue where the Checkbox component (built on RadioCheckboxBase) was maintaining its own internal state (this.state.isSelected) and toggling it immediately on click, even when acting as a controlled component (where isSelected is supplied as a prop).

This local state mutation caused synchronization bugs when the parent component chose not to update the prop on onChange (e.g. while awaiting popup confirmation), leaving the checkbox visually unchecked while isSelected={true} was still passed.

Solution

  1. Differentiate Controlled vs. Uncontrolled states:
    • If isSelected is provided by the parent, it is treated as controlled and serves as the single source of truth (getIsSelected()). Clicking does not change the visual state internally unless the parent updates the prop.
    • If isSelected is omitted, it behaves as uncontrolled and falls back to a local state (localIsSelected) that toggles on click.
  2. Modernize lifecycle:
    • Removed the deprecated componentWillReceiveProps method which was previously attempting to sync state from props incorrectly.
  3. Tests:
    • Added automated unit tests to verify both uncontrolled toggling and controlled state preservation.

Comment thread src/base/RadioCheckboxBase.js Outdated
if (this.props.isSelected !== isSelected) {
this.setState({ isSelected })
}
getIsSelected() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the API contract for every RadioCheckboxBase consumer, not just Checkbox. Before this patch, passing isSelected={false} still allowed the component to toggle internally after mount. With getIsSelected() treating any defined isSelected as controlled, isSelected={false} now freezes the visual state unless the parent writes the new value back on every onChange. That is a breaking behavior change for existing Radio/Switch/Checkbox usages built around the previous semantics. If the goal is only to fix the controlled-checkbox sync bug, this needs a narrower compatibility strategy or an explicit API change.

expect(wrapper.find('input').instance().checked).toBe(true)
})

it('acts as controlled when isSelected prop is provided', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new coverage only exercises the controlled case with isSelected={true}. The regression-prone case here is isSelected={false}, because that is the scenario that now changes behavior from "starts unchecked, then toggles internally" to fully controlled/non-toggling. Please add a test for isSelected={false} so this contract change is either caught or made explicit.

@viraj0075 viraj0075 force-pushed the fix-checkbox-controlled-state branch from 78fff08 to 3fcf61a Compare May 25, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Checkbox checked state isn't reflecting checked state as expected

1 participant