Repository: mui/material-ui (TypeScript)
Date: 2019-11-09 18:48:02+00:00
Task type: Bug Fix

Issue:
[Autocomplete] freeSolo is not actually usable
- [x] The issue is present in the latest release.
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Current Behavior 😯

If you use an Autocomplete with `freeSolo` the `onChange` is never fired, unless you click one one of the predefined autocomplete items or clear the field. So you can't actually use it like a search field, etc... because the component using it never gets the field's value.

Additionally even if you type out one of the predefined items, onChange is not fired unless you actually click/select the autocomplete item. This is mildly strange for normal Autocomplete where it clears the text but feels completely broken when using `freeSolo` where the text is not cleared and you expect the value has changed.
<!-- Describe what happens instead of the expected behavior. -->

## Expected Behavior 🤔

In `freeSolo` mode `onChange` should fire on every `input` key press like a normal `TextField` does.

Additionally, outside of `freeSolo` Autocomplete should probably fire `onChange` when you type out one of the options and blur.

## Steps to Reproduce 🕹

[![Edit Autocomplete freeSolo test](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/create-react-app-5zb0e?fontsize=14&view=preview)
Steps:

1. Add an `<Autocomplete freeSolo />` component like one of the demos in the documentation
2. Add a `onChange` and `value` connected to state to it
3. Try typing in the field, the controlled value will not update but the field text will

## Context 🔦

I wanted to use the Autocomplete to add a single tag filter to a videos list page. The tag can be arbitrary because I only plan to load the most common tags from the API.

## Your Environment 🌎

| Tech        | Version |
| ----------- | ------- |
| Material-UI | v4.5.2  |
| Material-UI Lab | v4.0.0-alpha.30  |
| React       | 16.8.6 |
| Browser     | Chrome |



Issue discussion:
> In freeSolo mode onChange should fire on every input key press like a normal TextField does.

The onChange and value props for the freeSolo are meant to account  for user confirmation (click / enter). The google place demo access the input value like this:
https://github.com/mui-org/material-ui/blob/c28697d0ba4c891f826133a77e58f298dc50c4bb/docs/src/pages/components/autocomplete/GoogleMaps.js#L89-L106

We could add an `onInputValueChange` prop to make it easier to merge the change event with custom input implementations. 

> Additionally, outside of freeSolo Autocomplete should probably fire onChange when you type out one of the options and blur.

It's what the `autoSelect` prop is for.
> The onChange and value props for the freeSolo are meant to account for user confirmation (click / enter). The google place demo access the input value like this:

This demo is broken. Using onChange on the TextField is not sufficient to keep up with the current input of the field.

I've modified that example with the debug output from my demo. You'll see that the clear button does not work correctly, it clears the field but the value the host gets from onChange is not cleared.

[![Edit Material demo](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/material-demo-9c49w?fontsize=14)

It's hard to do in a demo because CodeSandbox doesn't have the right origin to make the demo work with the Google API. However using the DevTools on the demo page I can also confirm that clicking one of the autocomplete options also does not update inputValue.
@dantman Interesting, it seems that the input onChange can be used to know when to query the API for a new value and that the Autocomplete onChange to know when the user selects a new value.

What would you like to change and what problem would it solve? Thanks.
> Interesting, it seems that the input onChange can be used to know when to query the API for a new value and that the Autocomplete onChange to know when the user selects a new value.

Except that's not actually true, because pressing the clear button should have the same effect as "⌘+A, Delete", it's user input and should result in a new API value just like typing.

You do make note of a valid use case. Wanting to re-fetch data/change value only for user input in the field (typing and clearing) but not when the field is updated via autoComplete or selection of auto-complete. Like with a location search where the full label and the text you'd search are completely disconnected and it would work strangely if you tried to search with the value you get back from autocomplete.

However I believe this type of situation is the minority of `freeSolo` use cases. For every other type of filtering, searching, etc... I can think of that you might do with `freeSolo` you'd want your value to keep up with whatever the current input of the field is no matter how the user did it (typing or autocompleting).

How about this, we change onChange's behaviour in `freeSolo` to work for the common and most React-like case (onChange is fired such that `value` will always match whatever is displayed in the text field, like a normal controlled input) and we add other events to cover the scenarios where you don't always want your value to be in-sync with the text field.

* onChange: Fires on typing, clearing, autoComplete "preview", and item selection
* onInputChange: Fires on typing and clearing, does not include the autoComplete "preview" text
* onSelect (or onItemChange?): Only fires on item selection (via manual selection or autoSelect)

Outside of `freeSolo` where values outside of the options are not valid, `onChange` of course will work more like a `<select>`'s `onChange` and would not fire without a valid selection:

* onChange: Only fires on item selection (via manual selection or autoSelect)
* onInputChange: Fires on typing and clearing, does not include the autoComplete "preview" text
* onSelect (or onItemChange?): Only fires on item selection (via manual selection or autoSelect)

This will actually be an improvement for the use-case you describe because `onInputChange` is properly fired when clearing the field, but onChange on the text input is not fired.

Alternatively since `onChange` and `onSelect` overlap, perhaps `onChange` could fire with `autoComplete`'s "previews" as they are valid selections, but `onSelect` would not fire for a preview until the selection is confirmed (item selection, blur, etc...).

Then `onChange` can be used for live updating previews (like how `onChange` normally behaves in react). And `onSelect` can be used for when the changed needs to be "committed".
> Except that's not actually true, because pressing the clear button should have the same effect as "⌘+A, Delete", it's user input and should result in a new API value just like typing.

@dantman You are right, this even causes a bug where if you clear the input with the button and press the down arrow key, it shows the suggestions for the preview query:

- type "a" in the input
- click on the clear button
- press down arrow

Expected output: no popup, actual output: popup. We need to fix that. Regarding your suggestions:

- I strongly think that we shouldn't change the `onChange` behavior. I think that it should trigger when a user has manifested a strong intention to change the "selected" value.
- I don't think that autocomplete "preview" should trigger an event. It's meant to be a different "view" of the data.
- I like the addition of a prop like the *onInputChange* one you propose. Alternative wording would be *onInputValueChange* or *onSearch*. 

However, to go back to the initial problem, what are you trying to implement that the current implement stops you from? I'm interested in this part:

> I wanted to use the Autocomplete to add a single tag filter to a videos list page. The tag can be arbitrary because I only plan to load the most common tags from the API.

Thank you!
> * I strongly think that we shouldn't change the `onChange` behavior. I think that it should trigger when a user has manifested a strong intention to change the "selected" value.

Honestly in another scenario I might agree, but IMHO that doesn't really fit with the scenario we have. For better or worse in React `onChange` is not for commit like `change`, it's a live reflection of the value like `input`. If it weren't for that I'd agree that `onChange` should be the "commit" event and the live updating event should have a different name.

IMHO while Autocomplete with `freeSolo={false}` may be akin to a select with a search box. Autocomplete with `freeSolo={true}` is more equivalent to a TextField with an optional autocompletion box. So it makes sense for onChange to keep up with whatever is in the input for a controlled component.

> However, to go back to the initial problem, what are you trying to implement that the current implement stops you from? I'm interested in this part:
> 
> > I wanted to use the Autocomplete to add a single tag filter to a videos list page. The tag can be arbitrary because I only plan to load the most common tags from the API.

Personally I have a video list page and I'm adding a tag filter input with autocomplete of popular tags to it. There are no other filters yet (or ever, not sure) and there is no "submit". Input is debounced and live-updates the video list. I don't want to wait for the input to be "committed" because typing a tag name without selecting from the list is a valid behaviour and there are no other fields that would encourage the user to blur the input triggering a "commit" of what they typed. If I waited for "commit" not only would it add an extra delay waiting for that user interaction, there's a fair chance a user may type out a tag and then sit there confused not realizing they need to click/tab out of the input before the video list will update.

<img width="739" alt="Screen Shot 2019-11-01 at 3 03 18 PM" src="https://user-images.githubusercontent.com/53399/68059252-100dcd00-fcb9-11e9-89e0-d171e9343052.png">

I'm currently working around the limitations by using the following, which IMHO I consider a really ugly hack. Personally I want to fix the issues in MUI Autocomplete so I can remove the hack, but I'm also thinking about the other use cases for Autocomplete that need fixing.

```js
<Autocomplete
  freeSolo
  options={tagsList}
  loading={tagsLoading}
  onChange={onTagInputChange}
  value={tagInputValue}
  renderInput={props => (
    <TextField
      {...props}
      label="Tag (filter)"
      variant="outlined"
      fullWidth
      inputProps={{
        ...props.inputProps,
        onChange: e => {
          onTagInputChange(e, e.target.value || null);
          props.inputProps.onChange(e);
        },
      }}
    />
  )}
/>
```
@dantman Awesome, thank you for sharing the use case. It sounds like you implement a "search as you type" like experience. The `onChange` event would probably only be useful to change the URL.
I have implemented a similar UX in the past with react-autosuggest. Eventually, we change it to be more Google search like (https://www.theverge.com/2017/7/26/16034844/google-kills-off-instant-search-for-mobile-consistency).

@kennethmervin01 in #18169 raises a similar concern, he wants to be able to control the textbox value.

In this context, what do you guys think if we add this API to the useAutocomplete.js (and Autocomplete.js). Would it solve your use cases?

```tsx
/**
 * The input value.
 */
inputValue?: string;
/**
 * Callback fired when the input value changes.
 */
onInputValueChange?: (event: React.ChangeEvent<{}>, value: any) => void;
```

We might want to add a third reason argument in the future, but I think that we should wait to hear valid use cases for it.
@oliviertassinari I am running into the same issue and that would solve it - thank you! Do you have any idea when this will be added? I am trying to avoid switching to a new library right now but we have tight deadlines coming up and this is required functionality, so I thought it was worth checking. :) 
@dallashuggins This issue is labeled "good first issue" as there is a clear resolution path. We tend not to work on such issues, for a matter of efficiency. What alternative did you consider? :)
@oliviertassinari Super appreciate the quick response! Gotcha, understood. Originally I was going to use `react-autosuggest`, but I switched to Material UI once I discovered the Autosuggest component, especially since we're already using Material UI for certain components. Other than an issue with the `Mui-focused` class name not being added to the TextField component used in `renderInput`, it's worked great. I have already implemented the Autosuggest component, and found a fix for the onFocus issue, and really want to find a solution for this if at all possible. Our use-case is users need to be able to select a lender from a long list of lenders (hence the autofill) and also add a new lender name if a lender is not found. Both names and IDs are valid to send in the API request we're sending on form submission. 

Solution patch:
diff --git a/docs/pages/api/autocomplete.md b/docs/pages/api/autocomplete.md
--- a/docs/pages/api/autocomplete.md
+++ b/docs/pages/api/autocomplete.md
@@ -46,6 +46,7 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
 | <span class="prop-name">groupBy</span> | <span class="prop-type">func</span> |  | If provided, the options will be grouped under the returned string. The groupBy value is also used as the text for group headings when `renderGroup` is not provided.<br><br>**Signature:**<br>`function(options: any) => string`<br>*options:* The option to group. |
 | <span class="prop-name">id</span> | <span class="prop-type">string</span> |  | This prop is used to help implement the accessibility logic. If you don't provide this prop. It falls back to a randomly generated id. |
 | <span class="prop-name">includeInputInList</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the highlight can move to the input. |
+| <span class="prop-name">inputValue</span> | <span class="prop-type">string</span> |  | The input value. |
 | <span class="prop-name">ListboxComponent</span> | <span class="prop-type">elementType</span> | <span class="prop-default">'ul'</span> | The component used to render the listbox. |
 | <span class="prop-name">loading</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the component is in a loading state. |
 | <span class="prop-name">loadingText</span> | <span class="prop-type">node</span> | <span class="prop-default">'Loading…'</span> | Text to display when in a loading state. |
@@ -53,6 +54,7 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
 | <span class="prop-name">noOptionsText</span> | <span class="prop-type">node</span> | <span class="prop-default">'No options'</span> | Text to display when there are no options. |
 | <span class="prop-name">onChange</span> | <span class="prop-type">func</span> |  | Callback fired when the value changes.<br><br>**Signature:**<br>`function(event: object, value: any) => void`<br>*event:* The event source of the callback<br>*value:* null |
 | <span class="prop-name">onClose</span> | <span class="prop-type">func</span> |  | Callback fired when the popup requests to be closed. Use in controlled mode (see open).<br><br>**Signature:**<br>`function(event: object) => void`<br>*event:* The event source of the callback. |
+| <span class="prop-name">onInputChange</span> | <span class="prop-type">func</span> |  | Callback fired when the input value changes.<br><br>**Signature:**<br>`function(event: object, value: string) => void`<br>*event:* The event source of the callback.<br>*value:* null |
 | <span class="prop-name">onOpen</span> | <span class="prop-type">func</span> |  | Callback fired when the popup requests to be opened. Use in controlled mode (see open).<br><br>**Signature:**<br>`function(event: object) => void`<br>*event:* The event source of the callback. |
 | <span class="prop-name">open</span> | <span class="prop-type">bool</span> |  | Control the popup` open state. |
 | <span class="prop-name">options</span> | <span class="prop-type">array</span> | <span class="prop-default">[]</span> | Array of options. |
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
@@ -181,6 +181,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
     groupBy,
     id: idProp,
     includeInputInList = false,
+    inputValue: inputValueProp,
     ListboxComponent = 'ul',
     loading = false,
     loadingText = 'Loading…',
@@ -188,6 +189,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
     noOptionsText = 'No options',
     onChange,
     onClose,
+    onInputChange,
     onOpen,
     open,
     options = [],
@@ -487,6 +489,10 @@ Autocomplete.propTypes = {
    * If `true`, the highlight can move to the input.
    */
   includeInputInList: PropTypes.bool,
+  /**
+   * The input value.
+   */
+  inputValue: PropTypes.string,
   /**
    * The component used to render the listbox.
    */
@@ -521,6 +527,13 @@ Autocomplete.propTypes = {
    * @param {object} event The event source of the callback.
    */
   onClose: PropTypes.func,
+  /**
+   * Callback fired when the input value changes.
+   *
+   * @param {object} event The event source of the callback.
+   * @param {string} value
+   */
+  onInputChange: PropTypes.func,
   /**
    * Callback fired when the popup requests to be opened.
    * Use in controlled mode (see open).
diff --git a/packages/material-ui-lab/src/TreeView/TreeView.js b/packages/material-ui-lab/src/TreeView/TreeView.js
--- a/packages/material-ui-lab/src/TreeView/TreeView.js
+++ b/packages/material-ui-lab/src/TreeView/TreeView.js
@@ -39,7 +39,6 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
     onNodeToggle,
     ...other
   } = props;
-  const [expandedState, setExpandedState] = React.useState(defaultExpanded);
   const [tabable, setTabable] = React.useState(null);
   const [focused, setFocused] = React.useState(null);
 
@@ -48,7 +47,8 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
   const firstCharMap = React.useRef({});
 
   const { current: isControlled } = React.useRef(expandedProp !== undefined);
-  const expanded = (isControlled ? expandedProp : expandedState) || [];
+  const [expandedState, setExpandedState] = React.useState(defaultExpanded);
+  const expanded = (isControlled ? expandedProp : expandedState) || defaultExpandedDefault;
 
   if (process.env.NODE_ENV !== 'production') {
     // eslint-disable-next-line react-hooks/rules-of-hooks
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
@@ -107,6 +107,10 @@ export interface UseAutocompleteProps {
    * If `true`, the highlight can move to the input.
    */
   includeInputInList?: boolean;
+  /**
+   * The input value.
+   */
+  inputValue?: string;
   /**
    * If true, `value` must be an array and the menu will support multiple selections.
    */
@@ -127,8 +131,11 @@ export interface UseAutocompleteProps {
   onClose?: (event: React.ChangeEvent<{}>) => void;
   /**
    * Callback fired when the input value changes.
+   *
+   * @param {object} event The event source of the callback.
+   * @param {string} value
    */
-  onInputChange?: React.ChangeEventHandler<HTMLInputElement>;
+  onInputChange?: (event: React.ChangeEvent<{}>, value: any) => void;
   /**
    * Callback fired when the popup requests to be opened.
    * Use in controlled mode (see open).
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -68,10 +68,12 @@ export default function useAutocomplete(props) {
     groupBy,
     id: idProp,
     includeInputInList = false,
+    inputValue: inputValueProp,
     multiple = false,
     onChange,
     onClose,
     onOpen,
+    onInputChange,
     open: openProp,
     options = [],
     value: valueProp,
@@ -165,10 +167,13 @@ export default function useAutocomplete(props) {
   });
   const value = isControlled ? valueProp : valueState;
 
-  const [inputValue, setInputValue] = React.useState('');
+  const { current: isInputValueControlled } = React.useRef(inputValueProp != null);
+  const [inputValueState, setInputValue] = React.useState('');
+  const inputValue = isInputValueControlled ? inputValueProp : inputValueState;
+
   const [focused, setFocused] = React.useState(false);
 
-  const resetInputValue = useEventCallback(newValue => {
+  const resetInputValue = useEventCallback((event, newValue) => {
     let newInputValue;
     if (multiple) {
       newInputValue = '';
@@ -195,10 +200,14 @@ export default function useAutocomplete(props) {
     }
 
     setInputValue(newInputValue);
+
+    if (onInputChange) {
+      onInputChange(event, newInputValue);
+    }
   });
 
   React.useEffect(() => {
-    resetInputValue(value);
+    resetInputValue(null, value);
   }, [value, resetInputValue]);
 
   const { current: isOpenControlled } = React.useRef(openProp != null);
@@ -404,7 +413,7 @@ export default function useAutocomplete(props) {
       handleClose(event);
     }
 
-    resetInputValue(newValue);
+    resetInputValue(event, newValue);
 
     selectedIndexRef.current = -1;
   };
@@ -590,7 +599,7 @@ export default function useAutocomplete(props) {
     if (autoSelect && selectedIndexRef.current !== -1) {
       handleValue(event, filteredOptions[selectedIndexRef.current]);
     } else if (!freeSolo) {
-      resetInputValue(value);
+      resetInputValue(event, value);
     }
 
     handleClose(event);
@@ -612,6 +621,10 @@ export default function useAutocomplete(props) {
     }
 
     setInputValue(newValue);
+
+    if (onInputChange) {
+      onInputChange(event, newValue);
+    }
   };
 
   const handleOptionMouseOver = event => {
diff --git a/packages/material-ui/src/RadioGroup/RadioGroup.js b/packages/material-ui/src/RadioGroup/RadioGroup.js
--- a/packages/material-ui/src/RadioGroup/RadioGroup.js
+++ b/packages/material-ui/src/RadioGroup/RadioGroup.js
@@ -7,28 +7,10 @@ import RadioGroupContext from './RadioGroupContext';
 const RadioGroup = React.forwardRef(function RadioGroup(props, ref) {
   const { actions, children, name, value: valueProp, onChange, ...other } = props;
   const rootRef = React.useRef(null);
-  const { current: isControlled } = React.useRef(valueProp != null);
-  const [valueState, setValue] = React.useState(() => {
-    return !isControlled ? props.defaultValue : null;
-  });
-
-  React.useImperativeHandle(
-    actions,
-    () => ({
-      focus: () => {
-        let input = rootRef.current.querySelector('input:not(:disabled):checked');
-
-        if (!input) {
-          input = rootRef.current.querySelector('input:not(:disabled)');
-        }
 
-        if (input) {
-          input.focus();
-        }
-      },
-    }),
-    [],
-  );
+  const { current: isControlled } = React.useRef(valueProp != null);
+  const [valueState, setValue] = React.useState(props.defaultValue);
+  const value = isControlled ? valueProp : valueState;
 
   if (process.env.NODE_ENV !== 'production') {
     // eslint-disable-next-line react-hooks/rules-of-hooks
@@ -49,7 +31,25 @@ const RadioGroup = React.forwardRef(function RadioGroup(props, ref) {
     }, [valueProp, isControlled]);
   }
 
-  const value = isControlled ? valueProp : valueState;
+  React.useImperativeHandle(
+    actions,
+    () => ({
+      focus: () => {
+        let input = rootRef.current.querySelector('input:not(:disabled):checked');
+
+        if (!input) {
+          input = rootRef.current.querySelector('input:not(:disabled)');
+        }
+
+        if (input) {
+          input.focus();
+        }
+      },
+    }),
+    [],
+  );
+
+  const handleRef = useForkRef(ref, rootRef);
 
   const handleChange = event => {
     if (!isControlled) {
@@ -60,14 +60,13 @@ const RadioGroup = React.forwardRef(function RadioGroup(props, ref) {
       onChange(event, event.target.value);
     }
   };
-  const context = { name, onChange: handleChange, value };
-
-  const handleRef = useForkRef(ref, rootRef);
 
   return (
-    <FormGroup role="radiogroup" ref={handleRef} {...other}>
-      <RadioGroupContext.Provider value={context}>{children}</RadioGroupContext.Provider>
-    </FormGroup>
+    <RadioGroupContext.Provider value={{ name, onChange: handleChange, value }}>
+      <FormGroup role="radiogroup" ref={handleRef} {...other}>
+        {children}
+      </FormGroup>
+    </RadioGroupContext.Provider>
   );
 });
 
diff --git a/packages/material-ui/src/Slider/Slider.js b/packages/material-ui/src/Slider/Slider.js
--- a/packages/material-ui/src/Slider/Slider.js
+++ b/packages/material-ui/src/Slider/Slider.js
@@ -369,15 +369,36 @@ const Slider = React.forwardRef(function Slider(props, ref) {
     ...other
   } = props;
   const theme = useTheme();
-  const { current: isControlled } = React.useRef(valueProp != null);
   const touchId = React.useRef();
   // We can't use the :active browser pseudo-classes.
   // - The active state isn't triggered when clicking on the rail.
   // - The active state isn't transfered when inversing a range slider.
   const [active, setActive] = React.useState(-1);
   const [open, setOpen] = React.useState(-1);
+
+  const { current: isControlled } = React.useRef(valueProp != null);
   const [valueState, setValueState] = React.useState(defaultValue);
   const valueDerived = isControlled ? valueProp : valueState;
+
+  if (process.env.NODE_ENV !== 'production') {
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    React.useEffect(() => {
+      if (isControlled !== (valueProp != null)) {
+        console.error(
+          [
+            `Material-UI: A component is changing ${
+              isControlled ? 'a ' : 'an un'
+            }controlled Slider to be ${isControlled ? 'un' : ''}controlled.`,
+            'Elements should not switch from uncontrolled to controlled (or vice versa).',
+            'Decide between using a controlled or uncontrolled Slider ' +
+              'element for the lifetime of the component.',
+            'More info: https://fb.me/react-controlled-components',
+          ].join('\n'),
+        );
+      }
+    }, [valueProp, isControlled]);
+  }
+
   const range = Array.isArray(valueDerived);
   const instanceRef = React.useRef();
   let values = range ? [...valueDerived].sort(asc) : [valueDerived];
diff --git a/packages/material-ui/src/Tooltip/Tooltip.js b/packages/material-ui/src/Tooltip/Tooltip.js
--- a/packages/material-ui/src/Tooltip/Tooltip.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.js
@@ -107,17 +107,38 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) {
   } = props;
   const theme = useTheme();
 
-  const [openState, setOpenState] = React.useState(false);
   const [, forceUpdate] = React.useState(0);
   const [childNode, setChildNode] = React.useState();
   const ignoreNonTouchEvents = React.useRef(false);
-  const { current: isControlled } = React.useRef(openProp != null);
   const defaultId = React.useRef();
   const closeTimer = React.useRef();
   const enterTimer = React.useRef();
   const leaveTimer = React.useRef();
   const touchTimer = React.useRef();
 
+  const { current: isControlled } = React.useRef(openProp != null);
+  const [openState, setOpenState] = React.useState(false);
+  let open = isControlled ? openProp : openState;
+
+  if (process.env.NODE_ENV !== 'production') {
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    React.useEffect(() => {
+      if (isControlled !== (openProp != null)) {
+        console.error(
+          [
+            `Material-UI: A component is changing ${
+              isControlled ? 'a ' : 'an un'
+            }controlled Tooltip to be ${isControlled ? 'un' : ''}controlled.`,
+            'Elements should not switch from uncontrolled to controlled (or vice versa).',
+            'Decide between using a controlled or uncontrolled Tooltip ' +
+              'element for the lifetime of the component.',
+            'More info: https://fb.me/react-controlled-components',
+          ].join('\n'),
+        );
+      }
+    }, [openProp, isControlled]);
+  }
+
   if (process.env.NODE_ENV !== 'production') {
     // eslint-disable-next-line react-hooks/rules-of-hooks
     React.useEffect(() => {
@@ -164,30 +185,11 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) {
     };
   }, []);
 
-  if (process.env.NODE_ENV !== 'production') {
-    // eslint-disable-next-line react-hooks/rules-of-hooks
-    React.useEffect(() => {
-      if (isControlled !== (openProp != null)) {
-        console.error(
-          [
-            `Material-UI: A component is changing ${
-              isControlled ? 'a ' : 'an un'
-            }controlled Tooltip to be ${isControlled ? 'un' : ''}controlled.`,
-            'Elements should not switch from uncontrolled to controlled (or vice versa).',
-            'Decide between using a controlled or uncontrolled Tooltip ' +
-              'element for the lifetime of the component.',
-            'More info: https://fb.me/react-controlled-components',
-          ].join('\n'),
-        );
-      }
-    }, [openProp, isControlled]);
-  }
-
   const handleOpen = event => {
     // The mouseover event will trigger for every nested element in the tooltip.
     // We can skip rerendering when the tooltip is already open.
     // We are using the mouseover event instead of the mouseenter event to fix a hide/show issue.
-    if (!isControlled && !openState) {
+    if (!isControlled) {
       setOpenState(true);
     }
 
@@ -333,8 +335,6 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) {
   );
   const handleRef = useForkRef(children.ref, handleOwnRef);
 
-  let open = isControlled ? openProp : openState;
-
   // There is no point in displaying an empty tooltip.
   if (title === '') {
     open = false;
diff --git a/packages/material-ui/src/internal/SwitchBase.js b/packages/material-ui/src/internal/SwitchBase.js
--- a/packages/material-ui/src/internal/SwitchBase.js
+++ b/packages/material-ui/src/internal/SwitchBase.js
@@ -55,6 +55,7 @@ const SwitchBase = React.forwardRef(function SwitchBase(props, ref) {
   } = props;
   const { current: isControlled } = React.useRef(checkedProp != null);
   const [checkedState, setCheckedState] = React.useState(Boolean(defaultChecked));
+  const checked = isControlled ? checkedProp : checkedState;
 
   const muiFormControl = useFormControl();
 
@@ -79,14 +80,14 @@ const SwitchBase = React.forwardRef(function SwitchBase(props, ref) {
   };
 
   const handleInputChange = event => {
-    const checked = event.target.checked;
+    const newChecked = event.target.checked;
 
     if (!isControlled) {
-      setCheckedState(checked);
+      setCheckedState(newChecked);
     }
 
     if (onChange) {
-      onChange(event, checked);
+      onChange(event, newChecked);
     }
   };
 
@@ -98,7 +99,6 @@ const SwitchBase = React.forwardRef(function SwitchBase(props, ref) {
     }
   }
 
-  const checked = isControlled ? checkedProp : checkedState;
   const hasLabelFor = type === 'checkbox' || type === 'radio';
 
   return (
diff --git a/packages/material-ui/src/utils/useEventCallback.js b/packages/material-ui/src/utils/useEventCallback.js
--- a/packages/material-ui/src/utils/useEventCallback.js
+++ b/packages/material-ui/src/utils/useEventCallback.js
@@ -12,5 +12,5 @@ export default function useEventCallback(fn) {
   useEnhancedEffect(() => {
     ref.current = fn;
   });
-  return React.useCallback(event => (0, ref.current)(event), []);
+  return React.useCallback((...args) => (0, ref.current)(...args), []);
 }


Test patch:
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
@@ -549,4 +549,34 @@ describe('<Autocomplete />', () => {
       expect(textbox.selectionEnd).to.equal(3);
     });
   });
+
+  describe('controlled input', () => {
+    it('controls the input value', () => {
+      const handleChange = spy();
+      function MyComponent() {
+        const [, setInputValue] = React.useState('');
+        const handleInputChange = (event, value) => {
+          handleChange(value);
+          setInputValue(value);
+        };
+        return (
+          <Autocomplete
+            inputValue=""
+            onInputChange={handleInputChange}
+            renderInput={params => <TextField autoFocus {...params} />}
+          />
+        );
+      }
+
+      const { getByRole } = render(<MyComponent />);
+
+      const textbox = getByRole('textbox');
+      expect(handleChange.callCount).to.equal(1);
+      expect(handleChange.args[0][0]).to.equal('');
+      fireEvent.change(textbox, { target: { value: 'a' } });
+      expect(handleChange.callCount).to.equal(2);
+      expect(handleChange.args[1][0]).to.equal('a');
+      expect(textbox.value).to.equal('');
+    });
+  });
 });