Kiran
Kiran

Reputation: 431

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!

Upvotes: 10

Views: 5599

Answers (4)

Filip Kov&#225;č
Filip Kov&#225;č

Reputation: 194

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>
);

Upvotes: 13

Saulo Felipe
Saulo Felipe

Reputation: 33

Try this...

None of the alternatives worked for me. So I used this and it worked perfectly:

First I set all my routes in an array:

const routes = [
   <Route ...>,
   <Route ...>,
   <Route ...>
]

Then perform a map function:

{
  condition === true 
    ? routes.map(myRoute => myRoute)
    : <Redirect to="/"/>
}

It works perfectly!!

Upvotes: -1

Oscar
Oscar

Reputation: 11

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).

Upvotes: 1

Emanuel Andrada
Emanuel Andrada

Reputation: 149

I was dealing with something similar. I had this:

<Switch>
  {aCondition && (
    <>
      <Route ... />
      <Route ... />
    </>
  )}

which brought me that same warning. Seeing your code I saw that you had a fragment too and in the "else" you haven't. So I tried with:

{aCondition ? (
  <> 
    <Route ... />
    <Route ... />
  </>
) : (
  <></>
)}

and it works just fine. It seems that having a conditional fragment confuses the switch.

Upvotes: 1

Related Questions