Home > Software design >  Why can't React's forwardRef work well with Material UI's styled?
Why can't React's forwardRef work well with Material UI's styled?

Time:01-24

When using forwardRef with styled, I see this odd error : "React Hook ... cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function."

For example:

import { styled } from '@mui/material/styles';
import { useTranslation } from '../lib/i18n';

const FooComponent = styled(({ className }) => {
  const { t } = useTranslation();
  return <div className={ className }>{ t('This is fine') }</div>;
})(() => ({
  // CSS styles...
});
import { forwardRef } from 'react';
import { useTranslation } from '../lib/i18n';

const FooComponent = forwardRef(((), ref) => {
  const { t } = useTranslation();
  return <div ref={ref}>{ t('This is fine, too!') }</div>;
});
import { forwardRef } from 'react';
import { styled } from '@mui/material/styles';
import { useTranslation } from '../lib/i18n';

const FooComponent = forwardRef( styled(({ className }, ref) => {
  const { t } = useTranslation();    // ERROR here!
  return <div ref={ref} className={ className }>{ t('This is NOT fine') }</div>; 
})(() => ({
  // CSS styles...
})));

Why is there an error in the 3rd component? Is the only solution to have a custom ref prop?

CodePudding user response:

The main issue with your code is that styled() doesn't expect a second ref parameter, and doesn't know what to do with it. In contrast, forwardRef() does expect you to provide a function with a second ref parameter.

So to solve your problem the way you apply forwardRef() and styled() should be swapped (note that parentheses also slightly change).

const FooComponent = styled( forwardRef(({ className }, ref) => {
  const { t } = useTranslation();
  return <div ref={ref} className={ className }>{ t('This is NOT fine') }</div>; 
}))(() => ({
  // CSS styles...
}));

// or maybe easier to understand

const UnstyledFooComponent = forwardRef(({ className }, ref) => {
  const { t } = useTranslation();
  return <div ref={ref} className={ className }>{ t('This is NOT fine') }</div>; 
});

const styling = (() => ({
  // CSS styles...
});

const FooComponent = styled(UnstyledFooComponent)(styling);

This will pass the the (props, ref) => ... callback to forwardRef(), which understands the second ref parameter.

We'll then pass the resulting component to styled().

It's then up to styled() to pass the ref, from the trickery that styled() does, down to your component. Luckily styled() keeps this into account and creates a styled component that forwards the ref down.

If styled() didn't forward the ref down things would get a lot more complicated.

const { useRef, useEffect, forwardRef } = React;
const { styled } = MaterialUI;

const FooComponent = styled(forwardRef(({ className }, ref) => {
  const { t } = I18n.useTranslation();
  return <div ref={ref} className={ className }>{ t('This is fine') }</div>;
}))(() => ({
  backgroundColor: "red"
}));

function App() {
  const fooRef = useRef();

  useEffect(() => {
    console.log(fooRef.current);
  }, []);
  
  return <FooComponent className="foo" ref={fooRef} />;
}

// mock `useTranslation()`
const I18n = React.createContext();
I18n.useTranslation = function () {
  const translations = React.useContext(I18n);

  const t = React.useCallback((text) => {
    if (!(text in translations))
      throw new Error(`no translation found for "${text}"`);

    return translations[text];
  }, [translations]);
      
  return { t, translations };
};

const translations = {
  nl: {
    "This is fine": "Dit is oké",
  }
};

ReactDOM
  .createRoot(document.querySelector("#root"))
  .render(
    <React.StrictMode>
      <I18n.Provider value={translations.nl}>
        <App />
      </I18n.Provider>
    </React.StrictMode>
  );
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/@mui/material@5/umd/material-ui.development.js"></script>
<div id="root"></div>

  • Related