Repository: mui/material-ui (TypeScript)
Date: 2023-06-06 20:40:12+00:00
Task type: Bug Fix

Issue:
[Tooltip] TypeError: Cannot read property 'className' of undefined
I am getting this error. Code is crashing.

https://codesandbox.io/s/basictooltip-material-demo-forked-7ftwd

<img width="1010" alt="Screenshot 2019-10-31 at 17 50 39" src="https://user-images.githubusercontent.com/2432618/67948413-79350800-fc07-11e9-8bcc-a06d8c187ec8.png">
Here are my imports:

```jsx
import * as React from "react";
import Tooltip from "@material-ui/core/Tooltip";

export default function BasicTooltip() {
  return <Tooltip title="Delete">Hello</Tooltip>;
}
```


Issue discussion:
Check the documentation, the Tooltip requires an element as a child that forward the ref, for instance:

```jsx
<Tooltip title="Foo">
  <span>Bar</span>
</Tooltip>
```
Maybe that could be the error message if there is no child element present.
> Maybe that could be the error message if there is no child element present.

@elitan You should already have an explicit error in the console client-side. But yeah, server-side, it's a lot trickier.

In #17480, we explore the need for better handling the thrown error that isn't helpful.
Agreed, I had this exact error, had to wrap my text inside a `<Button>`
I bumped into this when I had 2 components inside the tooltip. I'm guessing Tooltip does not know how do deal with that. I removed one component and the problem went away.
@cyrfer The tooltip requires a React element that forward refs, a node isn't supported.
@thebazshah in the tooltip u have to required atleast one tag or react element but dont use more than one if u use then again this show same error
like:

```jsx
<Tooltip title="Delete">
     <button onClick={additem}><AddShoppingCartIcon /></button>
    <br />
     </Tooltip>
```

this is show error
   
if u remove <br/> then this error not shown
The same toolbar buttons in https://github.com/gregnb/mui-datatables/blob/master/src/components/TableToolbar.js as well as the material UI examples have the same implementation, which for me in customToolBar option renders this error. 

The code i'm referring to from source is below. This should work in custom toolbar as well. 
`<Tooltip title={"custom icon"}>
          <IconButton className={classes.iconButton} onClick={this.handleClick}>
            <AddIcon className={classes.deleteIcon} />
          </IconButton>`
Somewhere between customToolBar option prop and the actual rendering the forward ref is not happening. Workaround would be to not use tooltip and implement the functionality explicitly but that would not be ideal.  
Ran into this issue myself just recently, and the error message isn't all that helpful.  It would be great if Material UI were to raise a more verbose, detailed error, such as "`<Tooltip> requires a child element; strings and fragments may not be used`".  (Not sure what React calls the non-element children, so I'm referring to it as "strings".)

It'd probably be helpful to also note that the Tooltip component doesn't error if you pass a fragment (i.e. `<>Hello</>`) as a child element, but it will not attach a tooltip.
@vinyldarkscratch The prop types warning in the console should say exact this. 
@eps1lon 24 upvotes on https://github.com/mui-org/material-ui/issues/18119#issuecomment-548379346. What do you think about reoping #17480 to not throw when a required prop is missing? Basically give more chance for developers to check there console to see what's wrong without seeing a big error that catches the attention. 
I see the prop types warning you're referring to now.  I also updated my comment a bit to add more/better wording.  (To someone like me who isn't as experienced with React, my first reaction to the prop types error is "but wait, I have a child, so why isn't it working?")
@vinyldarkscratch Considering the prop types we have, a Fragment should also warn. It seems we are good here

https://github.com/mui-org/material-ui/blob/b0d77d040e84584c627df77573e09ab85be354d6/packages/material-ui/src/Tooltip/Tooltip.js#L621
Open a new issue with a repro please, thanks.
I'm reopening, many developers are facing this pain. I think that we have an opportunity to improve the DX when the children are invalid:

```jsx
import * as React from 'react';
import Tooltip from '@material-ui/core/Tooltip';

export default function BasicTooltip() {
  return <Tooltip title="Delete">Hello</Tooltip>;
}
```

We could either render null

```diff
diff --git a/packages/material-ui/src/Tooltip/Tooltip.js b/packages/material-ui/src/Tooltip/Tooltip.js
index dde86e84b2..cdd4c7696f 100644
--- a/packages/material-ui/src/Tooltip/Tooltip.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.js
@@ -514,6 +514,10 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
     nameOrDescProps['aria-labelledby'] = open && !titleIsString ? id : null;
   }

+  if (!React.isValidElement(children)) {
+    return null;
+  }
+
   const childrenProps = {
     ...nameOrDescProps,
     ...other,
```

Or have a graceful fallback:

```diff
diff --git a/packages/material-ui/src/Tooltip/Tooltip.js b/packages/material-ui/src/Tooltip/Tooltip.js
index dde86e84b2..44d5334452 100644
--- a/packages/material-ui/src/Tooltip/Tooltip.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.js
@@ -220,7 +220,7 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
   const { theme, isRtl, ...props } = useThemeProps({ props: inProps, name: 'MuiTooltip' });
   const {
     arrow = false,
-    children,
+    children: childrenProp,
     describeChild = false,
     disableFocusListener = false,
     disableHoverListener = false,
@@ -245,6 +245,8 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
     ...other
   } = props;

+  const children = React.isValidElement(childrenProp) ? childrenProp : <span>{childrenProp}</span>;
+
   const [childNode, setChildNode] = React.useState();
   const [arrowRef, setArrowRef] = React.useState(null);
   const ignoreNonTouchEvents = React.useRef(false);
```

and update the prop-types/types/description to document the span added when a simple text is provided. This is what Ant Design or Chakra are doing. This sounds simpler. I would encourage the latter.
I don't believe the added side-effect is a good solution. This is yet another attempt at silencing real issues that will not help developers.
@eps1lon Yeah, the second option (extra span) sounds a lot better than the first one (render null to give more emphasis to the prop-type warning)
> Yeah, the second option (extra span) sounds a lot better than the first one (render null to give more emphasis to the prop-type warning)

Both options are equally bad. There is an author error that needs attention not silently doing something unintended.
I don't follow. What's wrong with automatically adding a wrapper DOM node (and forwarding props, ref to it) if the developer didn't provide one? The one downside I can think of is that we need to document the behavior clearly. Developers need to know about its existence as it impacts the DOM output.
> What's wrong with automatically adding a wrapper DOM node (and forwarding props, ref to it) if the developer didn't provide one? 

Branching the behavior depending on the element type. It is surprising, makes the component harder to reason about and has side-effects since we support all of CSS.
> Branching the behavior depending on the element type. It is surprising, makes the component harder to reason about and has side-effects since we support all of CSS.

@eps1lon OK, In this case, I think that we can keep the prop-type to discourage hidden branching. It's a conservative approach, the wrapper has to be the developer's side.

At the same time, we can improve the DX by allowing developers to quickly try the tooltip out and avoid a distracting throw with:

```diff
diff --git a/packages/material-ui/src/Tooltip/Tooltip.js b/packages/material-ui/src/Tooltip/Tooltip.js
index dde86e84b2..44d5334452 100644
--- a/packages/material-ui/src/Tooltip/Tooltip.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.js
@@ -220,7 +220,7 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
   const { theme, isRtl, ...props } = useThemeProps({ props: inProps, name: 'MuiTooltip' });
   const {
     arrow = false,
-    children,
+    children: childrenProp,
     describeChild = false,
     disableFocusListener = false,
     disableHoverListener = false,
@@ -245,6 +245,8 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
     ...other
   } = props;

+  const children = React.isValidElement(childrenProp) ? childrenProp : <span>{childrenProp}</span>;
+
   const [childNode, setChildNode] = React.useState();
   const [arrowRef, setArrowRef] = React.useState(null);
   const ignoreNonTouchEvents = React.useRef(false);
```

Hey @oliviertassinari , is it still up for grab? 

@ankitmansinghani It's up for grab, but I'm not sure there is yet a common agreement on the best direction forward, so I'm changing the label. It's basically the same problem with a couple more components: https://github.com/mui-org/material-ui/issues/17480#issuecomment-533036454. I think that we should be consistent.

I think that the main issue with the current behavior is that the prop-type warning is server-side, not client-side with Next.js. It's hard to find and it only fires once. In many cases, developers are left to debug why it fails.

Option 1 is to do:

```diff
diff --git a/packages/material-ui/src/Tooltip/Tooltip.js b/packages/material-ui/src/Tooltip/Tooltip.js
index 3e319610bf..3329981969 100644
--- a/packages/material-ui/src/Tooltip/Tooltip.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.js
@@ -215,7 +215,7 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
   const { theme, isRtl, ...props } = useThemeProps({ props: inProps, name: 'MuiTooltip' });
   const {
     arrow = false,
-    children,
+    children: childrenProp,
     classes: classesProp,
     describeChild = false,
     disableFocusListener = false,
@@ -241,6 +241,8 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
     ...other
   } = props;

+  const children = React.Children.only(childrenProp);
+
   const [childNode, setChildNode] = React.useState();
   const [arrowRef, setArrowRef] = React.useState(null);
   const ignoreNonTouchEvents = React.useRef(false);
```

on the tooltip and the other component if we only want to have a sane error message:

<img width="748" alt="Capture d’écran 2021-06-14 à 19 46 38" src="https://user-images.githubusercontent.com/3165635/121935883-46427800-cd49-11eb-94f5-93c39c8ccea0.png">

The error message isn't perfect, it doesn't tell you what to change, nor, where to fix the issue, but it's already better than what currently happening. If we want to polish it further, we could have a custom throw method to be more explicit.

---

The alternative path, option 2, explored before in the thread was to have a less disruptive error message, a warning in the console.

```diff
diff --git a/packages/material-ui/src/Tooltip/Tooltip.js b/packages/material-ui/src/Tooltip/Tooltip.js
index 3e319610bf..a06fc936fd 100644
--- a/packages/material-ui/src/Tooltip/Tooltip.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.js
@@ -215,7 +215,7 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
   const { theme, isRtl, ...props } = useThemeProps({ props: inProps, name: 'MuiTooltip' });
   const {
     arrow = false,
-    children,
+    children: childrenProp,
     classes: classesProp,
     describeChild = false,
     disableFocusListener = false,
@@ -241,6 +241,8 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
     ...other
   } = props;

+  const children = React.isValidElement(childrenProp) ? childrenProp : <span>{childrenProp}</span>;
+
   const [childNode, setChildNode] = React.useState();
   const [arrowRef, setArrowRef] = React.useState(null);
   const ignoreNonTouchEvents = React.useRef(false);
```

<img width="1019" alt="Capture d’écran 2021-06-14 à 19 48 08" src="https://user-images.githubusercontent.com/3165635/121936045-7b4eca80-cd49-11eb-89bc-b3ff5aaef7a5.png">

It's clear (only one error), It tells you where the error is coming from (component trace) and how to fix the issue. 

--- 

cc @mui-org/core do you have a preference or even further options to suggest?
I would vote for Option 2, having a warning + wrapper makes sense to me. 
I lean toward option 2 but what's about styling on the `<span>`. I feel like there will be another issue about styling.

Example.

```js
<FlexComponent>
  <OtherComponent />
  <Tooltip>
    Hey
  </Tooltip>
</FlexComponent>
```

I propose another one. if not `React.isValidElement(childrenProp)`, don't render the tooltip but render the children and developer see the warning in console with useful message. This way, we don't need extra DOM and developer will need to fix the issue.
> but what's about styling on the `<span>`

@siriwatknp In option 2, developers get a warning, they need to wrap the string children on their side:

```jsx
<FlexComponent>
  <OtherComponent />
  <Tooltip>
    <span>Hey</span>
  </Tooltip>
</FlexComponent>
```

>  if not React.isValidElement(childrenProp), don't render the tooltip but render the children and developer see the warning in console with useful message

How would we implement this option 3? I think that we are looking for a quick win with not too many changes involved 😁
> In option 2, developers get a warning, they need to wrap the string children on their side:

Okay, I am good with that.
> I bumped into this when I had 2 components inside the tooltip. I'm guessing Tooltip does not know how do deal with that. I removed one component and the problem went away.

Thank you! This worked for me.
TypeError: Cannot read property 'className' of undefined
Still facing this issue, even though I have wrapped the component inside some other comp. 
![image](https://user-images.githubusercontent.com/47501732/128496686-c67ec9fc-97d0-47a8-89b3-b47449ea5e5c.png)

> TypeError: Cannot read property 'className' of undefined
> Still facing this issue, even though I have wrapped the component inside some other comp.
> ![image](https://user-images.githubusercontent.com/47501732/128153904-9ddb6ece-9fd4-4d35-866a-7049f12af7e2.png)
> ![image](https://user-images.githubusercontent.com/47501732/128153782-2ce98a3e-b7d6-4538-bfa5-7c76801303d1.png)

yeah i also got the same message

Solution patch:
diff --git a/packages/mui-material/src/Tooltip/Tooltip.js b/packages/mui-material/src/Tooltip/Tooltip.js
--- a/packages/mui-material/src/Tooltip/Tooltip.js
+++ b/packages/mui-material/src/Tooltip/Tooltip.js
@@ -233,7 +233,7 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
   const props = useThemeProps({ props: inProps, name: 'MuiTooltip' });
   const {
     arrow = false,
-    children,
+    children: childrenProp,
     classes: classesProp,
     components = {},
     componentsProps = {},
@@ -263,6 +263,9 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
     ...other
   } = props;
 
+  // to prevent runtime errors, developers will need to provide a child as a React element anyway.
+  const children = React.isValidElement(childrenProp) ? childrenProp : <span>{childrenProp}</span>;
+
   const theme = useTheme();
   const isRtl = theme.direction === 'rtl';
 


Test patch:
diff --git a/packages/mui-material/src/Tooltip/Tooltip.test.js b/packages/mui-material/src/Tooltip/Tooltip.test.js
--- a/packages/mui-material/src/Tooltip/Tooltip.test.js
+++ b/packages/mui-material/src/Tooltip/Tooltip.test.js
@@ -994,6 +994,12 @@ describe('<Tooltip />', () => {
         'The `children` component of the Tooltip is not forwarding its props correctly.',
       );
     });
+
+    it('should warn when children is a string', () => {
+      expect(() => {
+        render(<Tooltip title="Hello World">Hello World</Tooltip>);
+      }).toErrorDev('Invalid prop `children` of type `string` supplied');
+    });
   });
 
   it('should use the same Popper.js instance between two renders', () => {