<Outlet /> fails to rerender with react router v6

Viewed 691

In the following code, the url changes but the content doesn't rerender until manual refresh. What am I doing wrong here? I could use props.children or something but don't really want to. My understanding of is that it should render the content of the nested elements under .

const LandingPage = () => {
        return (
            <div>
                <div>
                    buttons
                    <Button>
                        <Link to="/team1">team1</Link>
                    </Button>
                    <Button>
                        <Link to="/team2">team2</Link>
                    </Button>
                    <Button>
                        <Link to="/team3">team3</Link>
                    </Button>
                </div>
                <Outlet />
            </div>
        )
}

export default class Router extends Component<any> {

    state = {
        teams: [team1, team2, team3] as Team[]
    }

    public render() {
        return (
            <BrowserRouter>
                <Routes>
                    <Route path='/' element={<LandingPage />} >
                        {
                            this.state.teams.map(team => {
                                const path = `/${team.name.toLowerCase()}`
                                return (
                                    <Route path={path} element={
                                        <BaseTeam
                                            name={team.name}
                                            TL={team.TL}
                                            location={team.location}
                                            members={team.members}
                                            iconPath={team.iconPath}
                                        />
                                    } />)
                            })
                        }
                    </Route>
                </Routes>
            </BrowserRouter>
        )
    }
}
2 Answers

It seems the mapped routes are missing a React key. Add key={path} so each route is rendering a different instance of BaseTeam.

The main issue is that the BaseTeam component is the same "instance" for all the routes rendering it.

It should either also have a key prop specified so when the key changes BaseTeam is remounted and sets the name class property.

Example:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<LandingPage />}>
      {this.state.teams.map((team) => {
        const path = `/${team.name.toLowerCase()}`;
        return (
          <Route
            key={path} // <-- add missing React key
            path={path}
            element={(
              <BaseTeam
                key={path} // <-- add key to trigger remounting
                name={team.name}
              />
            )}
          />
        );
      })}
    </Route>
  </Routes>
</BrowserRouter>

Edit outlet-fails-to-rerender-with-react-router-v6

Or BaseTeam needs to be updated to react to the name prop updating. Use the componentDidUpdate lifecycle method to check the name prop against the current state, enqueue a state update is necessary.

Example:

class BaseTeam extends React.Component {
  state = {
    name: this.props.name
  };

  componentDidUpdate(prevProps) {
    if (prevProps.name !== this.props.name) {
      this.setState({ name: this.props.name });
    }
  }

  render() {
    return <div>{this.state.name}</div>;
  }
}

...

<BrowserRouter>
  <Routes>
    <Route path="/" element={<LandingPage />}>
      {this.state.teams.map((team) => {
        const path = `/${team.name.toLowerCase()}`;
        return (
          <Route
            key={path}
            path={path}
            element={<BaseTeam name={team.name} />}
          />
        );
      })}
    </Route>
  </Routes>
</BrowserRouter>

Edit outlet-fails-to-rerender-with-react-router-v6 (forked)

As you've found out in your code though, just rendering the props.name prop directly is actually the correct solution. It's a React anti-pattern to store passed props into local state. As you can see, it requires extra code to keep the props and state synchrononized.

Maybe signal to react library that a key has changed so that it needs to rerender the outlet

const LandingPage = () => {
    const location = useLocation(); // react-router-dom
    return (
        <div>
            <div>
                buttons
                <Button>
                    <Link to="/team1">team1</Link>
                </Button>
                <Button>
                    <Link to="/team2">team2</Link>
                </Button>
                <Button>
                    <Link to="/team3">team3</Link>
                </Button>
            </div>
            <Outlet key={location.pathname}/>
        </div>
    )}
Related