Repository: mui/material-ui (TypeScript)
Date: 2020-04-03 19:44:26+00:00
Task type: Bug Fix

Issue:
Cannot read property 'pulsate' of null
I'm seeing an error similar to this issue: https://github.com/mui-org/material-ui/issues/16810 when trying to provide a custom component to the ```<Tab />```  component

- [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.

## Steps to Reproduce 🕹
https://codesandbox.io/s/create-react-app-rjsfs

Steps:

1. Press the "tab" key

## Context 🔦

I'm trying to use Tabs that have both text and a clickable icon


Issue discussion:

Solution patch:
diff --git a/docs/src/pages/guides/composition/composition.md b/docs/src/pages/guides/composition/composition.md
--- a/docs/src/pages/guides/composition/composition.md
+++ b/docs/src/pages/guides/composition/composition.md
@@ -32,7 +32,7 @@ Material-UI allows you to change the root element that will be rendered via a pr
 
 ### How does it work?
 
-The component will render like this:
+The custom component will be rendered by Material-UI like this:
 
 ```js
 return React.createElement(props.component, props)
@@ -67,9 +67,11 @@ import { Link } from 'react-router-dom';
 function ListItemLink(props) {
   const { icon, primary, to } = props;
 
+  const CustomLink = props => <Link to={to} {...props} />;
+
   return (
     <li>
-      <ListItem button component={props => <Link to={to} {...props} />}>
+      <ListItem button component={CustomLink}>
         <ListItemIcon>{icon}</ListItemIcon>
         <ListItemText primary={primary} />
       </ListItem>
@@ -81,7 +83,7 @@ function ListItemLink(props) {
 ⚠️ However, since we are using an inline function to change the rendered component, React will unmount the link every time `ListItemLink` is rendered. Not only will React update the DOM unnecessarily, the ripple effect of the `ListItem` will also not work correctly.
 
 The solution is simple: **avoid inline functions and pass a static component to the `component` prop** instead.
-Let's change the `ListItemLink` to the following:
+Let's change the `ListItemLink` component so `CustomLink` always reference the same component:
 
 ```jsx
 import { Link } from 'react-router-dom';
@@ -89,7 +91,7 @@ import { Link } from 'react-router-dom';
 function ListItemLink(props) {
   const { icon, primary, to } = props;
 
-  const renderLink = React.useMemo(
+  const CustomLink = React.useMemo(
     () =>
       React.forwardRef((linkProps, ref) => (
         <Link ref={ref} to={to} {...linkProps} />
@@ -99,7 +101,7 @@ function ListItemLink(props) {
 
   return (
     <li>
-      <ListItem button component={renderLink}>
+      <ListItem button component={CustomLink}>
         <ListItemIcon>{icon}</ListItemIcon>
         <ListItemText primary={primary} />
       </ListItem>
@@ -108,8 +110,6 @@ function ListItemLink(props) {
 }
 ```
 
-`renderLink` will now always reference the same component.
-
 ### Caveat with prop forwarding
 
 You can take advantage of the prop forwarding to simplify the code.
@@ -173,18 +173,17 @@ wrapped component can't hold a ref.
 In some instances an additional warning is issued to help with debugging, similar to:
 > Invalid prop `component` supplied to `ComponentName`. Expected an element type that can hold a ref.
 
-
 Only the two most common use cases are covered. For more information see [this section in the official React docs](https://reactjs.org/docs/forwarding-refs.html).
 
 ```diff
-- const MyButton = props => <div role="button" {...props} />;
-+ const MyButton = React.forwardRef((props, ref) => <div role="button" {...props} ref={ref} />);
+-const MyButton = props => <div role="button" {...props} />;
++const MyButton = React.forwardRef((props, ref) => <div role="button" {...props} ref={ref} />);
 <Button component={MyButton} />;
 ```
 
 ```diff
-- const SomeContent = props => <div {...props}>Hello, World!</div>;
-+ const SomeContent = React.forwardRef((props, ref) => <div {...props} ref={ref}>Hello, World!</div>);
+-const SomeContent = props => <div {...props}>Hello, World!</div>;
++const SomeContent = React.forwardRef((props, ref) => <div {...props} ref={ref}>Hello, World!</div>);
 <Tooltip title="Hello, again."><SomeContent /></Tooltip>;
 ```
 
diff --git a/packages/material-ui/src/ButtonBase/ButtonBase.js b/packages/material-ui/src/ButtonBase/ButtonBase.js
--- a/packages/material-ui/src/ButtonBase/ButtonBase.js
+++ b/packages/material-ui/src/ButtonBase/ButtonBase.js
@@ -6,7 +6,6 @@ import { elementTypeAcceptingRef, refType } from '@material-ui/utils';
 import useForkRef from '../utils/useForkRef';
 import useEventCallback from '../utils/useEventCallback';
 import withStyles from '../styles/withStyles';
-import NoSsr from '../NoSsr';
 import useIsFocusVisible from '../utils/useIsFocusVisible';
 import TouchRipple from './TouchRipple';
 
@@ -278,6 +277,28 @@ const ButtonBase = React.forwardRef(function ButtonBase(props, ref) {
   const handleOwnRef = useForkRef(focusVisibleRef, buttonRef);
   const handleRef = useForkRef(handleUserRef, handleOwnRef);
 
+  const [mountedState, setMountedState] = React.useState(false);
+
+  React.useEffect(() => {
+    setMountedState(true);
+  }, []);
+
+  const enableTouchRipple = mountedState && !disableRipple && !disabled;
+
+  if (process.env.NODE_ENV !== 'production') {
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    React.useEffect(() => {
+      if (enableTouchRipple && !rippleRef.current) {
+        console.error(
+          [
+            'Material-UI: the `component` prop provided to ButtonBase is invalid.',
+            'Please make sure the children prop is rendered in this custom component.',
+          ].join('\n'),
+        );
+      }
+    }, [enableTouchRipple]);
+  }
+
   return (
     <ComponentProp
       className={clsx(
@@ -307,12 +328,10 @@ const ButtonBase = React.forwardRef(function ButtonBase(props, ref) {
       {...other}
     >
       {children}
-      <NoSsr>
-        {!disableRipple && !disabled ? (
-          /* TouchRipple is only needed client-side, x2 boost on the server. */
-          <TouchRipple ref={rippleRef} center={centerRipple} {...TouchRippleProps} />
-        ) : null}
-      </NoSsr>
+      {enableTouchRipple ? (
+        /* TouchRipple is only needed client-side, x2 boost on the server. */
+        <TouchRipple ref={rippleRef} center={centerRipple} {...TouchRippleProps} />
+      ) : null}
     </ComponentProp>
   );
 });


Test patch:
diff --git a/packages/material-ui/src/ButtonBase/ButtonBase.test.js b/packages/material-ui/src/ButtonBase/ButtonBase.test.js
--- a/packages/material-ui/src/ButtonBase/ButtonBase.test.js
+++ b/packages/material-ui/src/ButtonBase/ButtonBase.test.js
@@ -915,7 +915,7 @@ describe('<ButtonBase />', () => {
       PropTypes.resetWarningCache();
     });
 
-    it('warns on invalid `component` prop', () => {
+    it('warns on invalid `component` prop: ref forward', () => {
       // Only run the test on node. On the browser the thrown error is not caught
       if (!/jsdom/.test(window.navigator.userAgent)) {
         return;
@@ -936,5 +936,19 @@ describe('<ButtonBase />', () => {
         'Invalid prop `component` supplied to `ForwardRef(ButtonBase)`. Expected an element type that can hold a ref',
       );
     });
+
+    it('warns on invalid `component` prop: prop forward', () => {
+      const Component = React.forwardRef((props, ref) => (
+        <button type="button" ref={ref} {...props}>
+          Hello
+        </button>
+      ));
+
+      // cant match the error message here because flakiness with mocha watchmode
+      render(<ButtonBase component={Component} />);
+      expect(consoleErrorMock.messages()[0]).to.include(
+        'Please make sure the children prop is rendered in this custom component.',
+      );
+    });
   });
 });