Repository: mui/material-ui (TypeScript)
Date: 2019-04-30 06:46:38+00:00
Task type: Bug Fix

Issue:
[Zoom] Not accepting child component
First of all, Thanks for giving this beautiful framework. It has always been a great experience to work with Material-UI.

Today, I updated my project to `@material-ui/core@next`. Successfully removed all deprecated props. But, failed to solve this issue. Prior to v4 this was working.

- [ x ] This is not a v0.x issue.
- [ 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.

```jsx
// Input Field component
const InputField = ({
    field,
    form: { touched, errors },
    ...props
}) => {
    const error = errors[field.name];
    return (
        <TextField
            variant="outlined"
            margin="dense"
            error={touched && error ? true : false}
            helperText={touched && error ? `- ${error}` : null}
            fullWidth
            {...field}
            {...props}
        />
    );
};
```

## Expected Behavior 🤔
Zoom component is not accepting a `<Field/>` component from Formik as child.

```jsx
import { Field } from 'formik';

<Zoom in={show}>
         <Field
                 name="title"
                 type="text"
                 variant="outlined"
                 margin="dense"
                 component={InputField}
          />
</Zoom>
```
## Current Behavior 😯
Zoom should be given a `<div/>` to wrap the `<Field/>` component

```jsx
import { Field } from 'formik';

<Zoom in={show}>
           <div>
                  <Field
                            name="title"
                            type="text"
                            variant="outlined"
                            margin="dense"
                            component={InputField}
                   />
           </div>
</Zoom>
```

## Error
I am getting this error in the console.

```bash
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of `Transition`.
```

## Context 🔦
I am trying to animate fields/buttons in form whenever the form appears in the DOM.

## Your Environment 🌎

| Tech         | Version |
|--------------|---------|
| Material-UI  | ^4.0.0-beta.0  |
| React        |     ^16.8.6    |
| Browser      |     Chromium: 74.0.3729.108    |

Fun Fact: This is my first contribution 😋


Issue discussion:
@eps1lon Would this change work for you? It doesn't break any test but solves the warning here.
```diff
--- a/packages/material-ui/src/utils/reactHelpers.js
+++ b/packages/material-ui/src/utils/reactHelpers.js
@@ -18,11 +18,13 @@ export function useForkRef(refA, refB) {
    * This means react will call the old forkRef with `null` and the new forkRef
    * with the ref. Cleanup naturally emerges from this behavior
    */
-  return React.useCallback(
-    refValue => {
-      setRef(refA, refValue);
-      setRef(refB, refValue);
-    },
-    [refA, refB],
-  );
+  return React.useMemo(() => {
+    if (refA || refB) {
+      return refValue => {
+        setRef(refA, refValue);
+        setRef(refB, refValue);
+      };
+    }
+    return null;
+  }, [refA, refB]);
 }
```

Would it create ref leaks? It shouldn't.
It seems we won't fix this here: #15519.
I believe Formik should forward the ref. I have shared a workaround: https://github.com/jaredpalmer/formik/pull/478#issuecomment-487742488.

Solution patch:
diff --git a/packages/material-ui/src/utils/reactHelpers.js b/packages/material-ui/src/utils/reactHelpers.js
--- a/packages/material-ui/src/utils/reactHelpers.js
+++ b/packages/material-ui/src/utils/reactHelpers.js
@@ -14,15 +14,17 @@ export function setRef(ref, value) {
 
 export function useForkRef(refA, refB) {
   /**
-   * This will create a new function if the ref props change.
+   * This will create a new function if the ref props change and are defined.
    * This means react will call the old forkRef with `null` and the new forkRef
    * with the ref. Cleanup naturally emerges from this behavior
    */
-  return React.useCallback(
-    refValue => {
+  return React.useMemo(() => {
+    if (refA == null && refB == null) {
+      return null;
+    }
+    return refValue => {
       setRef(refA, refValue);
       setRef(refB, refValue);
-    },
-    [refA, refB],
-  );
+    };
+  }, [refA, refB]);
 }


Test patch:
diff --git a/packages/material-ui/src/utils/reactHelpers.test.js b/packages/material-ui/src/utils/reactHelpers.test.js
--- a/packages/material-ui/src/utils/reactHelpers.test.js
+++ b/packages/material-ui/src/utils/reactHelpers.test.js
@@ -2,6 +2,7 @@ import React from 'react';
 import { assert } from 'chai';
 import { spy } from 'sinon';
 import PropTypes from 'prop-types';
+import consoleErrorMock from 'test/utils/consoleErrorMock';
 import { isMuiElement, setRef, useForkRef } from './reactHelpers';
 import { Input, ListItemSecondaryAction, SvgIcon } from '..';
 import { mount } from 'enzyme';
@@ -63,6 +64,14 @@ describe('utils/reactHelpers.js', () => {
   });
 
   describe('useForkRef', () => {
+    beforeEach(() => {
+      consoleErrorMock.spy();
+    });
+
+    afterEach(() => {
+      consoleErrorMock.reset();
+    });
+
     it('returns a single ref-setter function that forks the ref to its inputs', () => {
       function Component(props) {
         const { innerRef } = props;
@@ -83,6 +92,91 @@ describe('utils/reactHelpers.js', () => {
       mount(<Component innerRef={outerRef} />);
 
       assert.strictEqual(outerRef.current.textContent, 'has a ref');
+      assert.strictEqual(consoleErrorMock.callCount(), 0);
+    });
+
+    it('forks if only one of the branches requires a ref', () => {
+      const Component = React.forwardRef(function Component(props, ref) {
+        const [hasRef, setHasRef] = React.useState(false);
+        const handleOwnRef = React.useCallback(() => setHasRef(true), []);
+        const handleRef = useForkRef(handleOwnRef, ref);
+
+        return <div ref={handleRef}>{String(hasRef)}</div>;
+      });
+
+      const wrapper = mount(<Component />);
+
+      assert.strictEqual(wrapper.containsMatchingElement(<div>true</div>), true);
+      assert.strictEqual(consoleErrorMock.callCount(), 0);
+    });
+
+    it('does nothing if none of the forked branches requires a ref', () => {
+      const Outer = React.forwardRef(function Outer(props, ref) {
+        const { children } = props;
+        const handleRef = useForkRef(children.ref, ref);
+
+        return React.cloneElement(children, { ref: handleRef });
+      });
+
+      Outer.propTypes = { children: PropTypes.element.isRequired };
+
+      function Inner() {
+        return <div />;
+      }
+
+      mount(
+        <Outer>
+          <Inner />
+        </Outer>,
+      );
+      assert.strictEqual(consoleErrorMock.callCount(), 0);
+    });
+
+    describe('changing refs', () => {
+      // use named props rather than ref attribute because enzyme ignores
+      // ref attributes on the root component
+      function Div(props) {
+        const { leftRef, rightRef, ...other } = props;
+        const handleRef = useForkRef(leftRef, rightRef);
+
+        return <div {...other} ref={handleRef} />;
+      }
+
+      Div.propTypes = {
+        leftRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+        rightRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+      };
+
+      it('handles changing from no ref to some ref', () => {
+        const wrapper = mount(<Div id="test" />);
+
+        assert.strictEqual(consoleErrorMock.callCount(), 0);
+
+        const ref = React.createRef();
+        wrapper.setProps({ leftRef: ref });
+
+        assert.strictEqual(ref.current.id, 'test');
+        assert.strictEqual(consoleErrorMock.callCount(), 0);
+      });
+
+      it('cleans up detached refs', () => {
+        const firstLeftRef = React.createRef();
+        const firstRightRef = React.createRef();
+        const secondRightRef = React.createRef();
+
+        const wrapper = mount(<Div leftRef={firstLeftRef} rightRef={firstRightRef} id="test" />);
+
+        assert.strictEqual(consoleErrorMock.callCount(), 0);
+        assert.strictEqual(firstLeftRef.current.id, 'test');
+        assert.strictEqual(firstRightRef.current.id, 'test');
+        assert.strictEqual(secondRightRef.current, null);
+
+        wrapper.setProps({ rightRef: secondRightRef });
+
+        assert.strictEqual(firstLeftRef.current.id, 'test');
+        assert.strictEqual(firstRightRef.current, null);
+        assert.strictEqual(secondRightRef.current.id, 'test');
+      });
     });
   });
 });