React Router V6 nested Routes with i18n

Viewed 935

I have a question about React Router V6 nested with i18n.

This is my first multi-language service.

const MainPage:React.FC = () => {

  const lang = i18n.language;

  return (
    <>
      <Wrapper>
        <Routes>
          {/* Main */}
          <Route path={`/`} element={<Home />}>
            <Route path={`${lang}`}>
              <Route path={`service`}>
                <Route path={'slack'} element={<Slack />} />
              </Route>
            </Route>
            {/* <Route path={`service/dooray`}element={<Dooray />} /> */}
            {/* <Route path={`contact`} element={<Contact />} /> */}

            {/* <Route path={`app/sign-in`} element={<SignIn />} /> */}
            {/* <Route path={`app/sign-up`} element={<SignUp />} /> */}
            {/* <Route path={`app/mail-code`} element={<MailCode />} /> */}
            {/* <Route path={`app/password/reset`} element={<PwdReset />} /> */}

            {/* <Route path={`policies/privac`} element={<Privacy />} /> */}
            {/* <Route path={`policies/terms`} element={<Terms />} /> */}
          </Route>
          {/* <Route path={`*`} element={<>NOT FOUND</>} /> */}
          {/* test */}
        </Routes>
      </Wrapper>
      <ParentModal />
    </>

If I enter localhost:3000/en, there is an error 'This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.'

How can I fix it..

I want /en => go to english page, /jp => go to japanese page


const MainPage:React.FC =() => {

...

<Route path={`/`} element={<Home />}>
    <Route path={`/${lang}/*`}>
       <Route path={`service`}>
           <Route path="slack" element={<Slack />} />
       </Route>
    </Route>
</Route>
}
const Home:React.FC = () => {

 return (
 <>
   ... UI, JSX
   <Outlet />
 </>
 )
}

I add a <Outlet />. But if I entered '/ko/service/slack', render <Home /> now


<Route path={`/`} element={<Home />}>
    <Route path="service">
       <Route path="slack" element={<Slack />} />
       <Route path="dooray" element={<Dooray />} />
    </Route>
</Route>

nested-routes doesn't work.. :(

2 Answers

Issue

The error 'This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.' means the parent route isn't rendering an Outlet component for the nested routes to be rendered into. The route rendering the Home component doesn't appear to be rendering an Outlet.

Solution

Update the Home component to render an Outlet. Note that Route components without an element prop will render an Outlet by default.

Example:

import { Outlet } from 'react-router-dom';

const Home = () => {
  ...

  return (
    <>
      ... home page UI/JSX ...
      <Outlet />
    </>
  );
};

...

const MainPage:React.FC = () => {
  const lang = i18n.language;

  return (
    <>
      <Wrapper>
        <Routes>
          {/* Main */}
          <Route path="/" element={<Home />}>
            <Route path={lang}>   // <-- renders Outlet by default
              <Route path="service"> // <-- renders Outlet by default
                <Route path="slack" element={<Slack />} />
              </Route>
            </Route>
            ...
          </Route>
          ...
          {/* test */}
        </Routes>
      </Wrapper>
      <ParentModal />
    </>
  );
};

Update

If the Home and Slack components are separate and independent, then move the Home component into an index route and simplify the routing to the Slack component.

<Routes>
  <Route path="/">
    <Route index element={<Home />} />
    <Route path={`${lang}/service/slack`} element={<Slack />} />
  </Route>
</Routes>

Edit react-router-v6-nested-routes-with-i18n

I had the exact same useCase (localize react router v6) and came up with the following LangRouter repository link:

const LangRouter = () => {
  const { i18n } = useTranslation(),
    { pathname, search, hash } = useLocation(),
    navigate = useNavigate(),
    availableLocales = ['en', 'ar'],
    defaultLocale = (
      getDefaultLanguage() === 'en' || getDefaultLanguage() === 'ar' ? getDefaultLanguage() : 'en'
    ) as string,
    pathnameLocale = pathname.substring(1, 3).toLowerCase(),
    [locale, setLocale] = useState(defaultLocale),
    loaderTimerRef = useRef<any>(),
    [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    loaderTimerRef.current = setTimeout(() => {
      setIsLoading(false);
      clearTimeout(loaderTimerRef.current);
    }, 300);
  }, []);

  useEffect(() => {
    if (availableLocales.includes(pathnameLocale)) {
      updateLocale(pathnameLocale);
    } else if (pathname === '/') {
      updateLocale(defaultLocale);
    }
    // eslint-disable-next-line
  }, [pathname]);

  useEffect(() => {
    let lang = defaultLocale;

    if (availableLocales.includes(pathnameLocale)) {
      lang = pathnameLocale;
      setLanguageHandler(lang);
    } else if (pathname === '/') {
      setLanguageHandler(lang);
    }
    // eslint-disable-next-line
  }, [locale]);

  const setLanguageHandler = (lang: string) => {
    if (lang === 'en') {
      i18n.changeLanguage('en-US');
    } else {
      i18n.changeLanguage('ar-SA');
    }
  };

  const updateLocale = (newLocale: string) => {
    const newPath = `/${newLocale}` + pathname.substring(3);

    if (locale !== newLocale) {
      if (newPath === `/${newLocale}/` || newPath === `/${newLocale}` || pathname === '/') {
        navigate(getHomePageUrl(newLocale));
      } else {
        navigate(`${newPath}${hash}${search}`);
      }
      setLocale(newLocale);
    } else if (newPath === `/${newLocale}/` || newPath === `/${newLocale}` || pathname === '/') {
      if (isAuthenticated()) {
        navigate(getHomePageUrl(newLocale));
      } else {
        navigate(getLoginPageUrl(newLocale));
      }
    }
  };

  if (isLoading) {
    return (
      <div className="loader-wrapper">
        <LoadingIcon />
      </div>
    );
  }

  return (
    <LocaleContext.Provider value={{ locale, setLocale: updateLocale }}>
      <Routes>
        <Route path={`/${locale}`} element={<App />}>
          {publicRoutes.map((el, i) => (
            <Route
              key={i}
              path={el.path(locale)}
              element={
                <PublicRouteGuard
                  restricted={el.restricted}
                  redirect={el.redirect ? el.redirect(locale) : undefined}
                >
                  {el.element}
                </PublicRouteGuard>
              }
            />
          ))}

          {privateRoutes.map((el, i) => (
            <Route
              key={i}
              path={el.path(locale)}
              element={
                el.permissions ? (
                  <RestrictedRouteGuard requiredPermissions={el.permissions}>
                    {el.element}
                  </RestrictedRouteGuard>
                ) : (
                  <PrivateRouteGuard>{el.element}</PrivateRouteGuard>
                )
              }
            >
              {el.children &&
                el.children.map((innerEl, innerI) => (
                  <Route key={innerI} path={innerEl.path(locale)} element={innerEl.element} />
                ))}
            </Route>
          ))}
        </Route>
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </LocaleContext.Provider>
  );
};

export default LangRouter;
Related