Search code examples
reactjsreact-routerreact-router-dom

<Outlet /> fails to rerender with react router v6


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

Solution

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