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>