Material-ui: Autocomplete immediately changes initial inputValue state

Created on 27 Jan 2020  路  43Comments  路  Source: mui-org/material-ui

When using Autocomplete as a controlled component and driving that state with onInputChange, the initial state is overwritten immediately to a blank string: ''.

  • [x] The issue is present in the latest release.
  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

Current Behavior 馃槸


Set up inputValue and onInputChange to make Autocomplete a controlled component with an initial value not equal to ''.
On startup, notice onInputChange is immediately called with a blank string and thus resets the initial value.

Expected Behavior 馃

The initial value of inputValue is retained.

Steps to Reproduce 馃暪

https://codesandbox.io/s/material-demo-2quum?fontsize=14&hidenavigation=1&theme=dark

Steps:

  1. Notice that the state is initialized to 'The Shawshank Redemption`
  2. Notice on start up that onInputChange(console logged) is called on startup, resetting the initial value to ''

Context 馃敠

The presence of this bug prevents me from using Autocomplete as a controlled component as it doesn't preserve my initial value

Your Environment 馃寧

| Tech | Version |
| ----------- | ------- |
| Material-UI | v4.9.0 |
| React | v16.12.0 |
| Browser | |
| TypeScript | |
| etc. | |

bug 馃悰 Autocomplete good first issue

Most helpful comment

Ok, thanks for the feedback. I think that we could wait a bit, to get more feedback from users before closing the issue. I would suspect that we will have more developers confused. I suspect that a new dedicated section for the controllable use cases would be beneficial and the best resolution.

All 43 comments

I have a feeling this goes hand-in-hand with this issue: https://github.com/mui-org/material-ui/issues/19318.

Thanks for opening this issue and providing a codesandbox explaining the issue.

Unfortunately I cannot observe the issue. In codesandbox.io/embed/material-demo-2quum?fontsize=14&hidenavigation=1&theme=dark nothing is logged to the console. Did you accidentally change the sandbox? I would recommend freezing the codesandbox after you opened the issue.

Could you include what browser and operating system you're using?

I apologize for the confusion - I was having issues with code sandbox saving my changes as you suggested. The posted link should be up to date now. I am on Linux/Chrome, I also tested on Linux/Firefox.

@willwill96 I have seen at least 5 developers fall into this trap so far, e.g. #19318.

There are two isolated states 1. value => onChange and 2. inputValue => onInputChange. You can't mix the controlled field and the controlled event, they work in pair.

@oliviertassinari Am I misunderstanding something? It seems like you are saying only use inputValue with onInputChange and only use value with onChange. In my codesandbox example posted, I only used inputValue with onInputChange, and still saw the same result of the initial value being reset

馃憢 Thanks for using Material-UI!

We use GitHub issues exclusively as a bug and feature requests tracker, however,
this issue appears to be a support request.

For support, please check out https://material-ui.com/getting-started/support/. Thanks!

If you have a question on StackOverflow, you are welcome to link to it here, it might help others.
If your issue is subsequently confirmed as a bug, and the report follows the issue template, it can be reopened.

I can't think of a way we could better teach this (leveraging the documentation and the component warnings). I assume StackOverflow is the best answer we can have (somebody explaining it there). Thanks for raising.

Well, actually, we could always try a more detailed controlled docs section.

It would be nice if one of the demos used onInputChange correctly as an example I think. That being said I am unsure how this is not classified as a bug.

I understand now why the ticket I linked (https://github.com/mui-org/material-ui/issues/19318) is not considered a bug, but the issue I posted here is not the same, and I believe I am using the Autocomplete in the proper way.

The code sandbox posted in this description, uses only inputValue => onInputChange as you suggested in your comment here: https://github.com/mui-org/material-ui/issues/19423#issuecomment-578822292 . (assuming i understood you correctly)

@willwill96 The value takes precedence over the inputValue.

@oliviertassinari Why does an undefined value prop take precedence over a defined inputValue prop?

I am not passing in the value or onChange prop into my codesandbox.

I could be mistaken, but I feel that you are addressing the ticket that I linked, and not the issue that I presented in this ticket.

@willwill96 The value is not undefined but null by default, hence an empty input value.
It seems that we could at least improve the API's documentation, in case you want to submit a pull request :)

Capture d鈥檈虂cran 2020-01-27 a虁 21 28 39

diff --git a/docs/pages/api/autocomplete.md b/docs/pages/api/autocomplete.md
index f6531069f..e158319ee 100644
--- a/docs/pages/api/autocomplete.md
+++ b/docs/pages/api/autocomplete.md
@@ -35,7 +35,7 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
 | <span class="prop-name">closeIcon</span> | <span class="prop-type">node</span> | <span class="prop-default">&lt;CloseIcon fontSize="small" /></span> | The i
con to display in place of the default close icon. |
 | <span class="prop-name">closeText</span> | <span class="prop-type">string</span> | <span class="prop-default">'Close'</span> | Override the default text for
 the *close popup* icon button.<br>For localization purposes, you can use the provided [translations](/guides/localization/). |
 | <span class="prop-name">debug</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the popup will ignore the
blur event if the input if filled. You can inspect the popup markup with your browser tools. Consider this option when you need to customize the component. |
-| <span class="prop-name">defaultValue</span> | <span class="prop-type">any<br>&#124;&nbsp;array</span> |  | The default input value. Use when the component i
s not controlled. |
+| <span class="prop-name">defaultValue</span> | <span class="prop-type">any<br>&#124;&nbsp;array</span> | <span class="prop-default">props.multiple ? [] : nul
l</span> | The default input value. Use when the component is not controlled. |
 | <span class="prop-name">disableClearable</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the input can't
 be cleared. |
 | <span class="prop-name">disableCloseOnSelect</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the popup w
on't close when a value is selected. |
 | <span class="prop-name">disabled</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the input will be disab
led. |
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
index 5c0ebf1ba..342c8edf0 100644
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
@@ -247,7 +247,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
     closeIcon = <CloseIcon fontSize="small" />,
     closeText = 'Close',
     debug = false,
-    defaultValue,
+    defaultValue = props.multiple ? [] : null,
     disableClearable = false,
     disableCloseOnSelect = false,
     disabled = false,
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index 6e5cfcb14..00e7ca020 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -85,7 +85,7 @@ export default function useAutocomplete(props) {
     blurOnSelect = false,
     clearOnEscape = false,
     debug = false,
-    defaultValue,
+    defaultValue = props.multiple ? [] : null,
     disableClearable = false,
     disableCloseOnSelect = false,
     disableListWrap = false,
@@ -193,7 +193,7 @@ export default function useAutocomplete(props) {

   const [value, setValue] = useControlled({
     controlled: valueProp,
-    default: defaultValue || (multiple ? [] : null),
+    default: defaultValue,
     name: componentName,
   });

@oliviertassinari I have put the PR up for that change, and I do think this helps clarify why the issue is happening and I appreciate the time you've put into this :)

I still think the way that these set of props interact together is slightly convoluted though.

This is the way I have had to write my Autocomplete to get the desired effect:

const MyComponent = ()=> {
  const [value, setValue] = useState('myInitialValue')
  return (
    <Autocomplete
      defaultValue={value}
      inputValue={value}
      onInputChange={(_, value: string) => {
        setValue(value)
      }} 
     />
  )
}

In this example, it's strange to me that I need to give the autocomplete a defaultValue prop despite it being a controlled component. Especially given that the doc for defaultValue says:

Use when the component is not controlled

In an ideal world I would be using value alongside onInputChange, but as you said, those don't work together (although I would argue they should if the component is controlled).

@willwill96 For context, why do you control the component for? (Your code snippet looks great).

We use Autocomplete as a free text input box that also offers suggestions. We control the component because we have a few buttons at the top of our component that need to reset the values to certain templated values.

If the snippet above looks good to you, then I think I'm fine closing this issue. I do think it is a little strange for defaultValue to be stateful, but I suppose that's not really that big of an issue.

Ok, thanks for the feedback. I think that we could wait a bit, to get more feedback from users before closing the issue. I would suspect that we will have more developers confused. I suspect that a new dedicated section for the controllable use cases would be beneficial and the best resolution.

In this example, it's strange to me that I need to give the autocomplete a defaultValue prop despite it being a controlled component. Especially given that the doc for defaultValue says...

Additionally, if you pass your Autocomplete a defaultValue, you will receive this warning:

Screen Shot 2020-02-03 at 9 44 34 PM

Regarding a potential solution, I would propose a diff close to this:

diff --git a/docs/src/pages/components/autocomplete/autocomplete.md b/docs/src/pages/components/autocomplete/autocomplete.md
index 6ec7d561c..c64ae8d0b 100644
--- a/docs/src/pages/components/autocomplete/autocomplete.md
+++ b/docs/src/pages/components/autocomplete/autocomplete.md
@@ -30,6 +30,15 @@ Choose one country between 248.

 {{"demo": "pages/components/autocomplete/CountrySelect.js"}}

+### Controllable states
+
+The component has two states that can be independently controlled:
+
+1. the "value" state with the `value`/`onChange` props combination.
+2. the "input value" state with the `inputValue`/`onInputChange` props combination.
+
+> 鈿狅笍 Do not try to swap the usage of these props.
+
 ## Free solo

 Set `freeSolo` to true so the textbox can contain any arbitrary value. The prop is designed to cover the primary use case of a search box with suggestions, e.g. Google search.
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index 9957ed76d..68bf22375 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -198,9 +198,12 @@ export default function useAutocomplete(props) {
     name: componentName,
   });

-  const { current: isInputValueControlled } = React.useRef(inputValueProp != null);
-  const [inputValueState, setInputValue] = React.useState('');
-  const inputValue = isInputValueControlled ? inputValueProp : inputValueState;
+  const [inputValue, setInputValue] = useControlled({
+    controlled: inputValueProp,
+    default: '',
+    name: componentName,
+    value: 'inputValue',
+  });

   const [focused, setFocused] = React.useState(false);

diff --git a/packages/material-ui/src/utils/useControlled.js b/packages/material-ui/src/utils/useControlled.js
index 76c455e8c..21851b6cd 100644
--- a/packages/material-ui/src/utils/useControlled.js
+++ b/packages/material-ui/src/utils/useControlled.js
@@ -1,7 +1,7 @@
 /* eslint-disable react-hooks/rules-of-hooks, react-hooks/exhaustive-deps */
 import React from 'react';

-export default function useControlled({ controlled, default: defaultProp, name }) {
+export default function useControlled({ controlled, default: defaultProp, name, state = 'value' }) {
   const { current: isControlled } = React.useRef(controlled !== undefined);
   const [valueState, setValue] = React.useState(defaultProp);
   const value = isControlled ? controlled : valueState;
@@ -11,7 +11,7 @@ export default function useControlled({ controlled, default: defaultProp, name }
       if (isControlled !== (controlled !== undefined)) {
         console.error(
           [
-            `Material-UI: A component is changing ${
+            `Material-UI: A component is changing a ${state} of ${
               isControlled ? 'a ' : 'an un'
             }controlled ${name} to be ${isControlled ? 'un' : ''}controlled.`,
+            'The nature of the state is determined during the first render, it's considered controlled if the value is not `undefined`.',
             'Elements should not switch from uncontrolled to controlled (or vice versa).',
@@ -29,7 +29,7 @@ export default function useControlled({ controlled, default: defaultProp, name }
       if (defaultValue !== defaultProp) {
         console.error(
           [
-            `Material-UI: A component is changing the default value of an uncontrolled ${name} after being initialized. ` +
+            `Material-UI: A component is changing the default ${state} of an uncontrolled ${name} after being initialized. ` +
               `To suppress this warning opt to use a controlled ${name}.`,
           ].join('\n'),
         );

I think I'm running into the exact same problem in my application. I created a sandbox to illustrate it (before I realized this issue already existed): https://codesandbox.io/s/loving-ritchie-ulvin

  • when using value/onChange, the state is not updated when entering free text
  • when using inputValue/onInputChange, the component is shown with an initial empty value

@goffioul That's my problem, too. As a workaround I added onChange also to the TextField.

@Maaartinus Well, that comes with its own set of issues. If I have the same onChange handler for both Autocomplete and TextField and the initial state value the empty string, then there will be an error generated on the first character entered, about the TextField switching from uncontrolled to controlled mode.

I also wanted to mention that the autocomplete popup behaves a bit strangely when using value/onChange (without also setting onChange on the TextField) and the initial value is part of the available options. In the provided sandbox, Option 1 is the initial value. On focusing the field, the autocomplete popup contains all options. Erase the 1 and enter 2 instead, only Option 2 is listed in the autocomplete popup . Erase the 2 and re-enter 1, all options are again listed in the autocomplete popup. Maybe there's a good reason behind that logic, but I find it confusing.

@bugzpodder I have some concerns with the logic in https://github.com/bugzpodder/ui-components/blob/07409c975c8dfea9ea37d7f129a8170abdc7daf1/src/common-suggest/common-suggest.tsx#L72-L81. The value and inputValue states are meant to be isolated.

As mentioned before about defaultValue, I'm facing the same problem using the useAutocomplete hook, there is a way to provide a function in that to setInputValue, or inside of this hooks, put an effect over inputValue to detect when the property changes and update the internal state together, because I was trying to do that and see that useAutocomplete has an internal state, so would be great set a value whenever I want.

Material ui suck

@oliviertassinari The PR #20403 does not really solve the problem. I've updated my codesandbox, please have a look: https://codesandbox.io/s/thirsty-hodgkin-n1ype.

When you use value/onChange, the state is not is not updated when entering free text. E.g. try to type foobar in the first field then blur it, the state name1 is not updated.

When you use inputValue/onInputChange, the initial name2 value is immediately cleared by the TextField (which was the original bug reported).

How do you recommend to handle that scenario? That is:

  1. not loose the initial value
  2. update the state whenever free text is entered.

@goffioul Interesting, you have uncovered a bug, thanks for raising :). In your first example, the input should be restored on blur.

Here is the wrong behavior:
ko

Here is the correct one:
ok

What do you think of this patch? Do you want to submit a pull request? :)

diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index 3cb1ca0c5..891d351f1 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -758,7 +758,7 @@ export default function useAutocomplete(props) {
       selectNewValue(event, filteredOptions[highlightedIndexRef.current], 'blur');
     } else if (autoSelect && freeSolo && inputValue !== '') {
       selectNewValue(event, inputValue, 'blur', 'freeSolo');
-    } else if (!freeSolo) {
+    } else {
       resetInputValue(event, value);
     }

How do you recommend to handle that scenario? That is:

  • not loose the initial value
  • update the state whenever free text is entered.

@goffioul When should the user confirm his choice?

How do you recommend to handle that scenario? That is:

  • not loose the initial value
  • update the state whenever free text is entered.

@goffioul When should the user confirm his choice?

Well, in my scenario, the user can enter free text. The dropdown is just a list of suggestions for quick entry, but the user should be able to enter any arbitrary text. If I'm following correctly, resetting the value on blur is not gonna fit my use case.

@goffioul freeSolor + autoSelect will fit your use case (if you don't want the user to confirm his choice). If you want him to confirm and have a little bit more of context: https://material-ui.com/components/autocomplete/#helper-message.

@oliviertassinari Indeed that seems to be close to what I want. I've updated the sandbox to use autoSelect. There's still some odd behaviors, which I'm not sure are expected. I would appreciate if you could comment on them:

  1. load the sandbox, and clear the first field using the X icon
  2. type hello, then use the popup icon => no dropdown menu is displayed, expected?
  3. blur the field
  4. click the popup icon => dropdown menu is displayed, expected to be different than 2 above?
  5. _without moving the mouse_ click again the popup icon to close the dropdown menu, then blur the field => hello should still be in the field
  6. click the popup icon, then move the mouse down, passing over the dropdown menu, which will highlight the menu items successively, move the move outside the dropdown menu area and click anywhere to blur the field => the last dropdown menu item that was highlighted is now in the field, expected? tbh, this last point is quite confusing from a UX point of view

Note that this doesn't include the change you have suggested, not sure there's an easy way to do that in sandbox.

Given the triviality of the change, do you really need me to do a PR for it? I don't mind not getting the credit for it :)

  1. Looks expected, the autocomplete value is empty, the input should filter the available options
  2. Looks expected, the autocomplete value is filled, the popup should list all the alternatives.
  3. Ok, I guess nothing to report?
  4. I agree, it can be confusing. I think that it should be the topic of another issue. As far as I know, it's a broader concern. We could imagine a solution where if the mouse leaves the option within 100ms, we restore the highlight to the previous position.

Given the triviality of the change, do you really need me to do a PR for it? I don't mind not getting the credit for it :)

@goffioul This change wouldn't have made it without you, so I think you deserve some credit, one commit sounds reasonable :). Also, it will happen, so you might as well make sure it doesn't harm your use case.

  1. What I find non-intuitive is that the behavior is different than in step 2, given that the content of the field is the same in both case. I know my QA guys would file a bug against me for that :)

  2. Do you want me to open a separate issue for that? From a user point of view, given that I haven't selected anything in the dropdown menu, just hovered it, it doesn't make sense to have the field content modified.

  1. Yes, please.

For my own information, do you plan to do anything about point 4 above? (same control, same content, but different behavior)

@goffioul The selected value is different, the context is different the behavior is different, 4. sound great?

We can just agree to disagree. I understand what you're saying with the fact that the selected value is different. But from a normal user point of view (without any technical knowledge of react/material-ui internals), the only thing he/she sees is that the value is the same in the field, and on one hand the popup icon gives you nothing, on the other hand it gives you the dropdown menu. And if you try to explain to that user that the entered value hasn't been really selected yet and that he/she needs to confirm it by blurring the field first, he/she will just look at you with that what-the-hell-is-he-talking-about face.

@goffioul In this case, the best alternative is not to use autoSelect. Force the user to confirm the selection of an option.

                        defaultValue = {props.type === "Edit" ? props.data : null}
                           onChange={(event, value) =>
                          AssignSuppAddress(value)

i am getting this error :
a component is changing the default value state of an uncontrolled Autocomplete after being initialized. To suppress this warning opt to use a controlled Autocomplete.

@oliviertassinari It's a shame this issue was closed because it's not fixed. onInputChange is called immediately on render with an empty string as the value. This means the initial inputValue gets cleared.

Look at how Twitch search works: https://www.twitch.tv/search?term=war
It automatically sets the search term to"war" and the dropdown options are objects.

There is no way to do this with the Autocomplete due to this bug.

@kaldune Set war as the value of the Autocomplete. You will get the correct behavior.

@oliviertassinari Appreciate your response and everything you've done for us by driving this project. What ended up working is leveraging the reason string sent in the callback.

For others who find this, the very first call to onInputChange sends "reset" as the reason. Our implementation is very customized and we only ever need to update state when the reason is "input".

  const [inputText, setInputText] = React.useState(getUrlParam('term') || '');

  <Autocomplete
    inputValue={inputText}
    onInputChange={(event, value, reason) => {
      if (reason === 'input') {
        setInputText(value);
      }
    }

@kaldune I would really encourage using the value prop over the inputValue for this use case.

@oliviertassinari Appreciate your response and everything you've done for us by driving this project. What ended up working is leveraging the reason string sent in the callback.

For others who find this, the very first call to onInputChange sends "reset" as the reason. Our implementation is very customized and we only ever need to update state when the reason is "input".

  const [inputText, setInputText] = React.useState(getUrlParam('term') || '');

  <Autocomplete
    inputValue={inputText}
    onInputChange={(event, value, reason) => {
      if (reason === 'input') {
        setInputText(value);
      }
    }

@kaldune thank you a ton :heart_eyes: This solved my issue as well!

I'm using the Autocomplete as a Google/Bing style search box:
image

The suggestions are populated by calls to a backend search service, with both the current searchText as well as the suggestions being kept in my Redux store, so it was also fairly customized from the get go but I couldn't for the life of me figure out why I kept getting the

Material-UI: a component is changing the controlled inputValue state of Autocomplete to be uncontrolled.

error though, when my component was controlled and I never indicate that it wasn't controlled. But I think you're right, and that the 'reset' onInputChange event was must have been setting my searchText to null or undefined which would a) cause my text to not stick around and b) cause Material UI to think I wanted an uncontrolled Autocomplete and would then throw errors when the text changed? ... That's as best as I can tell ... I ended up getting it to work with roughly this:

```jsx
const SearchBox = props => {
const clear = () => {
dispatch(setSearchText(""))
}

const onSearchTextHandler = (event, value, reason) => {
    if (reason === 'input') {
        dispatch(setSearchText(event.target.value))
        dispatch(getSuggestions())
    }
    if (reason === 'clear') {
        clear();
    }
}

return (
    <Autocomplete
        disableClearable
        getOptionLabel={(option) => option ? option.ProjectName ? option.ProjectName : "" : ""}
        classes={autoCompleteClasses}
        options={suggestions}
        loading={suggestionsLoading}
        onChange={onSuggestionSelect}
        freeSolo
        inputValue={searchText || ""}
        onInputChange={onSearchTextHandler}
        noOptionsText={"Start typing to see suggestions..."}
        renderOption={(option) => {
            return (
                <Grid container className={classes.suggestion}>
                    <Grid item xs={1}>
                        {option.TextPlain === option.ProjectName ? <Business color="disabled" /> : <AccountBox color="disabled" />}
                    </Grid>
                    <Grid item xs={10}>
                        <Grid container className={classes.suggestionText}>
                            <Grid item xs={12}>
                                <span className={classes.suggestionHeading} dangerouslySetInnerHTML={{ __html: option.TextMarkup }}></span>
                            </Grid>
                            <Grid item xs={11}>
                                {option.SecondaryText}
                            </Grid>
                        </Grid>
                    </Grid>
                </Grid>
            )
        }}
        renderInput={params => (
            <TextField
                {...params}
                fullWidth
                InputLabelProps={{
                    className: classes.inputLabel
                }}
                InputProps={{
                    ...params.InputProps,
                    endAdornment: (
                        <>
                            <IconButton size="small" onClick={(e) => { executeSearch(); e.target.blur(); }} >
                                <SearchIcon className={classes.searchIcon} />
                            </IconButton>
                            <IconButton size="small" onClick={(e) => { onClear(); e.target.blur(); }} >
                                <CloseIcon className={classes.searchIcon} />
                            </IconButton>
                            {/* {params.InputProps.endAdornment} */}
                        </>
                    )
                }}
                variant="outlined"
                onKeyDown={onKeyDownHandler}
                label="Find Projects"
            />
        )}
    >
    </Autocomplete>
)

}

````

Was this page helpful?
0 / 5 - 0 ratings

Related issues

TimoRuetten picture TimoRuetten  路  3Comments

newoga picture newoga  路  3Comments

revskill10 picture revskill10  路  3Comments

chris-hinds picture chris-hinds  路  3Comments

rbozan picture rbozan  路  3Comments