Skip to content

fix: uncontrolled to controlled input warning#1186

Open
krisxcrash wants to merge 2 commits intomainfrom
kw/fix/sdk-165
Open

fix: uncontrolled to controlled input warning#1186
krisxcrash wants to merge 2 commits intomainfrom
kw/fix/sdk-165

Conversation

@krisxcrash
Copy link
Contributor

Summary

Fixes the React warning: "A component is changing an uncontrolled input to be controlled" when Input (or TextInput) receives a value prop that is initially undefined and later becomes a defined string.

Changes

  • Input.tsx – When the caller passes a value prop, the underlying input now always receives a defined value (value ?? ''). If value is not passed at all, the input remains uncontrolled. This keeps the input consistently controlled whenever value is in props and prevents React from seeing a switch from undefined to defined.
  • TextInput.tsx – Passes value={value ?? ''} to Input so undefined never reaches Input from this path (defense-in-depth).
  • Input.test.tsx – Adds a test that renders Input with value={undefined} then rerenders with value="hello", and asserts the input stays in sync and that no uncontrolled/controlled warning is logged to console.error.

Backwards compatibility

  • Uncontrolled usage (no value prop) is unchanged.
  • Controlled usage with a defined value is unchanged.
  • The only behavioral change is for the previously broken case (controlled with value={undefined}), which now renders as an empty controlled input and no longer triggers the warning.

How to verify

Run Input tests:

npm run test -- --run src/components/Common/UI/Input/Input.test.tsx

Copy link
Member

@serikjensen serikjensen left a comment

Choose a reason for hiding this comment

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

A couple of things on this one

  • I think we need to be extremely cautious around changes that impact core components like this. we need to thoroughly test for regressions across our input components. This will include any components that use TextInput and any components that include the Input component
  • I think we should mark this as a breaking change and make it known definitively to our adapter customers that text based inputs now have the empty string passed as a default for value and don't get undefined

Comment on lines +31 to +52
it('stays controlled when value is initially undefined and then defined', () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

const { rerender } = renderWithProviders(
<Input placeholder="Controlled" value={undefined} onChange={() => {}} />,
)
const input = screen.getByRole('textbox')
expect(input).toHaveValue('')

rerender(<Input placeholder="Controlled" value="hello" onChange={() => {}} />)
expect(screen.getByRole('textbox')).toHaveValue('hello')

const errorCalls = errorSpy.mock.calls.filter(
call =>
typeof call[0] === 'string' &&
call[0].includes('uncontrolled') &&
call[0].includes('controlled'),
)
expect(errorCalls).toHaveLength(0)
errorSpy.mockRestore()
})

Copy link
Member

Choose a reason for hiding this comment

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

I think we can probably omit a test for this. we're already verifying that the input behaves as expected with the rest of our suite. there's a lot of errors we could test for the absence of but i think it's better to verify functionality than testing that things are not happening

...otherProps
} = resolvedProps

const ariaInputProps = 'value' in rawProps ? { ...otherProps, value: value ?? '' } : otherProps
Copy link
Member

Choose a reason for hiding this comment

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

I'm confused on this line, why is it checking rawProps? we should be able to just look at the destructured value from resolvedProps to determine if it is defined?

If we're just assigning a default here you should be able to do that when you destructure the resolved props? something like

    value = '',
    ...otherProps
  } = resolvedProps

and then you can pass value directly to the aria input props. that might be a bit more clear

...otherProps
} = resolvedProps

const isControlled = 'value' in rawProps
Copy link
Member

Choose a reason for hiding this comment

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

I'm still a little turned around on this one, maybe we could do a quick chat on it? 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

Same, should we be instead fixing this at the use-case? Seems like a defaultValues gap perhaps?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants