Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Router Warning: <Route> elements should not change from controlled to uncontrolled (or vice versa)

I am trying to understand the nuances behind a specific warning I see when working with React Router. I was trying to setup conditional routing based on whether or not the User was logged in or not. My code is as follows:

// AppRoutes.js
export const AppRoutes = ({ machine }) => {
  const [state] = useMachine(machine);
  
  let routes;
  
  if (state.matches('authenticated')) {
    routes = (
      <React.Fragment>
        <Route exact path="/"><HomePage /></Route>
        <Route path="/contacts"><ContactsList /></Route>
      </React.Fragment>
    );
  } else if (state.matches('unauthenticated')) {
    routes = (
      <Route path="/">
        <LoginPage service={state.children.loginMachine} />
      </Route>
    );
  } else {
    routes = null;
  }
  
  return (
    <BrowserRouter>
      <Switch>{routes}</Switch>
    </BrowserRouter>
  );
};

Internally, the HomePage component redirects to /contacts

// HomePage.js
export const HomePage = () => {
  return <Redirect to="/contacts" />;
};

Now with this code, the application works as I need it to, but I get a warning logged in the console:

Warning: <Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.

I did some research and the only thing I could find was this https://stackoverflow.com/a/52540643 which seems to indicate that conditionally rendering the routes is causing the issue. However, conditional rendering of routes is the whole point -- I don't want unauthenticated users accessing /contacts

Then after some playing around, I modified the source as below:

// AppRoutes.js
export const AppRoutes = ({ machine }) => {
  const [state] = useMachine(machine);
  
  let routes;
  
  if (state.matches('authenticated')) {
    routes = (
      <React.Fragment>
        <Route path="/home">
          <HomePage />
        </Route>
        <Route path="/contacts">
          <ContactsList />
        </Route>
        <Redirect to="/home" />
      </React.Fragment>
    );
  } else if (state.matches('unauthenticated')) {
    routes = (
      <React.Fragment>
        <Route path="/login">
          <LoginPage service={state.children.loginMachine} />
        </Route>
        <Redirect to="/login" />
      </React.Fragment>
    );
  }
  
  return (
    <BrowserRouter>
      <Switch>{routes}</Switch>
    </BrowserRouter>
  );
};

// HomePage.js
export const HomePage = () => {
  return <Redirect to="/contacts" />;
};

Now this code redirects authenticated users to /contacts and unauthenticated users to /login, and doesn't log any warnings.

Everything works great, except I still don't understand why the warning no longer appears and how is this different from what I was doing earlier. As far as I can see and understand, I am doing conditional rendering of routes in both versions of the code. Why does one log a warning, while the other doesn't?

Any guidance??

Thanks!

like image 528
Kiran Avatar asked Mar 03 '21 19:03

Kiran


People also ask

How do I restrict routes in react router?

The Route component from react-router is public by default but we can build upon it to make it restricted. We can add a restricted prop with a default value of false and use the condition if the user is authenticated and the route is restricted, then we redirect the user back to the Dashboard component.

Which component should be used to switch a route on a click?

The <Switch /> component will only render the first route that matches/includes the path. Once it finds the first route that matches the path, it will not look for any other matches.

Which props should you use to match exactly the path you have for routing?

The exact prop is used to define if there is an exactly the requested path.


2 Answers

According to docs: https://reactrouter.com/web/api/Switch/children-node

All children of a <Switch> should be <Route> or <Redirect> elements.

This means wrapping routes in <React.Fragment></React.Fragment> or simply <></> won't cut it.
Warning is present because these are not <Route> nor <Redirect> elements.

Solution 1: Wrapping in <Route> then another <Switch>

I don't like this solution because you have to repeat fallback Routes

<Switch>
  <Route exact path='/' component={Main} />
  {some_condition && (
    <Route path='/common'>
      <Switch>
        <Route exact path='/common/1' component={Secondary} />
        <Route exact path='/common/2' component={Ternary} />
        <Route component={NotFound} /> {/* this needs to be repeated here */}
      </Switch>
    </Route>
  )}
  <Route component={NotFound} />
</Switch>

Solution 2: Rendering routes as array

In the end this is what I went for. You have to include keys what is annoying but no extra elements needed.

<Switch>
  <Route exact path='/' component={Main} />
  {some_condition && [
    <Route exact path='/common/1' key='/common/1' component={Secondary} />,
    <Route exact path='/common/2' key='/common/2' component={Ternary} />
  ]}
  <Route component={NotFound} />
</Switch>

You can also format it like this:

let routes;
if (some_condition) {
  routes = [
    <Route exact path='/common/1' key='/common/1' component={Secondary} />,
    <Route exact path='/common/2' key='/common/2' component={Ternary} />
  ];
}
return (
  <Switch>
    <Route exact path='/' component={Main} />
    {routes}
    <Route component={NotFound} />
  </Switch>
);
like image 157
Filip Kováč Avatar answered Oct 16 '22 23:10

Filip Kováč


I just had the same issue, which Emanuel's answer did not fix for me. What worked is simply using <Switch> instead of <> (or <React.Fragment>) inside another Route.

Correct me if I'm wrong, but I think having nested Routes/Switches with react-router-dom is fine.

Example:

<Switch>
  <Route exact path='/' component={Main} />

  {some_condition && (
    <Route path='/common'>
      <Switch>
        <Route exact path='/common/path' component={Secondary} />
      </Switch>
    </Route>
  )}

  <Route path='/settings' component={Settings} />
</Switch>

However, this approach requires a common route path ("/common" in this example).

like image 1
Oscar Avatar answered Oct 16 '22 22:10

Oscar