Home > OS >  Does abstracting out a functional component containing a hook call violate the rules of hooks?
Does abstracting out a functional component containing a hook call violate the rules of hooks?

Time:03-12

I'm establishing two paths to switch language in my site in my react router. The components associated with those paths need just to perform a useEffect to set the language (though i18n) and then redirect to /. They look like this:

const SetEs = () => {
  const { i18n } = useTranslation();
  useEffect(() => {
    localStorage.setItem('preferredLanguage', 'es');
    i18n.changeLanguage('es');
  }, [])

  return (
    <Redirect to='/'/>
  )
}

const SetCat = () => {
  const { i18n } = useTranslation();
  useEffect(() => {
    localStorage.setItem('preferredLanguage', 'cat');
    i18n.changeLanguage('cat');
  }, [])

  return (
    <Redirect to='/'/>
  )
}

Then in the routes Switch...

<Switch>
  <Route exact path="/es" >
    <SetEs/>
  </Route>
  <Route exact path="/cat" >
    <SetCat/>
  </Route>
  ...
</Switch>

So far so good.

Now, looking back at the component's code I see clear that would be very kind to all my future "me"s and future coworkers to abstract a little that code redundancy. So I wrap it up in a function which takes the lang as parameter... hopefully it will simplify things out in the long run:

const SetLang = (lang: string) => () => {
  const { i18n } = useTranslation();
  useEffect(() => {
    localStorage.setItem('preferredLanguage', lang);
    i18n.changeLanguage(lang);
  }, [])

  return (
    <Redirect to='/'/>
  )
}
const SetEs = SetLang('es');
const SetCat = SetLang('cat');

But now I can't get pass this error: React Hook "useTranslation" cannot be called inside a callback

(I think) I know the rules of hooks. I know they shouldn't be called inside nested functions but I thought, up until now, that this referred only to functions nested within a component. Then there is the Don’t call Hooks from regular JavaScript functions but again, this is a regular function which returns a component which in turn calls the hook.

What am I missing? How do I get to be a good colleague nicely abstracting things out in a situation like this? What's the correct way of factoring components?

Thanks!

CodePudding user response:

Create a custom hook - useLang that accepts lang as an argument:

const useLang = (lang: string) => {
  const { i18n } = useTranslation()
  
  useEffect(() => {
    localStorage.setItem('preferredLanguage', lang)
    i18n.changeLanguage(lang);
  }, [lang])
}

Create a SetLang component that accepts lang as a prop, and calls useLang:

interface Props {
  lang: string;
}

const SetLang = ({ lang }: Props) => {
  useLang(lang)

  return (
    <Redirect to='/'/>
  )
}

Use it in your Route passing the lang prop:

<Switch>
  <Route exact path="/es" >
    <SetLang lang="es" />
  </Route>
  <Route exact path="/cat" >
    <SetLang lang="cat" />
  </Route>
  ...
</Switch>

You can also create wrapper components:

const SetEs = () => <SetLang lang="es" />;
const SetCat = () => <SetLang lang="cat" />;

and use them as you originally intended:

<Switch>
  <Route exact path="/es" >
    <SetEs />
  </Route>
  <Route exact path="/cat" >
    <SetCat />
  </Route>
  ...
</Switch>

CodePudding user response:

Maybe you can make a regular React component and pass lang as props ?

const SetLang = ({ lang }) => {
  const { i18n } = useTranslation();
  useEffect(() => {
    localStorage.setItem('preferredLanguage', lang);
    i18n.changeLanguage(lang);
  }, [])

  return (
    <Redirect to='/'/>
  )
}
<Switch>
  <Route exact path="/es" >
    <SetLang lang="es"/>
  </Route>
  <Route exact path="/cat" >
    <SetLang lang="cat"/>
  </Route>
  ...
</Switch>
  • Related