Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use custom functional components within Material-UI Menu

I am using Material-UI to design a react app that I'm building. I am trying to use my own custom functional component inside the <Menu> component and I'm getting a strange error:

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 ForwardRef(Menu).
    in TextDivider (at Filter.js:32)

The JSX where this occurs looks like this: (Line 32 is the TextDivider component)

<Menu>
    <TextDivider text="Filter Categories" />
    <MenuItem>...</MenuItem>
</Menu>

Where TextDivider is:

const TextDivider = ({ text }) => {
    const [width, setWidth] = useState(null);
    const style = {
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center'
    };

    const hrStyle = {
        width: `calc(100% - ${width}px)`,
        marginRight: 0,
        marginLeft: 0
    };

    const spanStyle = {
        color: '#555',
        whiteSpace: 'nowrap'
    };

    const ref = createRef();
    useEffect(() => {
        if (ref.current) {
            const width = ref.current.getBoundingClientRect().width + 35;
            console.log('width', width);
            setWidth(width);
        }
    }, [ref.current]);

    return (
        <div style={style}>
            <span ref={ref} style={spanStyle}>{ text }</span>
            <Divider className="divider-w-text" style={ hrStyle }/>
        </div>
    );
};

The strange part is that this component renders perfectly fine when set by itself in a regular container, but it seems to throw this error only when being rendered inside the <Menu> component.

Furthermore, I found it really strange that this error goes away completely if I just wrap TextDivider in a div like so:

<Menu>
    <div>
        <TextDivider text="Filter Categories" />
    </div>
    <MenuItem>...</MenuItem>
</Menu>

However, this feels like duct tape to me, and I'd rather understand the underlying problem of why I can't just render this component within the Menu. I'm not using any kind of Material-UI ref within my functional component and I'm not placing a ref on the <Divider> component within my <TextDivider> component so I don't understand why it's throwing this error.

Any insight into why this behavior is happening would be greatly appreciated.

I've tried using the useCallback hook, createRef(), useRef(), and read everything I could find on using hooks within a functional component. The error itself seems misleading because even the react docs themselves show using functional components refs here: https://reactjs.org/docs/hooks-reference.html#useref

like image 731
Blitz Avatar asked May 25 '19 17:05

Blitz


1 Answers

Menu uses the first child of the Menu as the "content anchor" for the Popover component used internally by Menu. The "content anchor" is the DOM element within the menu that Popover attempts to line up with the anchor element (the element outside of the menu that is the reference point for positioning the menu).

In order to leverage the first child as the content anchor, Menu adds a ref to it (using cloneElement). In order to not get the error you received (and for the positioning to work correctly), your function component needs to forward the ref to one of the components it renders (generally the outermost component — a div in your case).

When you use a div as the direct child of Menu, you don’t get the error because the div can receive a ref successfully.

The documentation SakoBu referred to contains details about how to forward refs to function components.

The result should look something like the following (but I’m answering this on my phone, so sorry if any syntax errors):

const TextDivider = React.forwardRef(({ text }, ref) => {
    const [width, setWidth] = useState(null);
    const style = {
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center'
    };

    const hrStyle = {
        width: `calc(100% - ${width}px)`,
        marginRight: 0,
        marginLeft: 0
    };

    const spanStyle = {
        color: '#555',
        whiteSpace: 'nowrap'
    };
    // Separate bug here.
    // This should be useRef instead of createRef.
    // I’ve also renamed this ref for clarity, and used "ref" for the forwarded ref.
    const spanRef = React.useRef();
    useEffect(() => {
        if (spanRef.current) {
            const width = spanRef.current.getBoundingClientRect().width + 35;
            console.log('width', width);
            setWidth(width);
        }
    }, [spanRef.current]);

    return (
        <div ref={ref} style={style}>
            <span ref={spanRef} style={spanStyle}>{ text }</span>
            <Divider className="divider-w-text" style={ hrStyle }/>
        </div>
    );
});

You may also find the following answer useful:

What's the difference between `useRef` and `createRef`?

like image 53
Ryan Cogswell Avatar answered Nov 14 '22 08:11

Ryan Cogswell