Search code examples
reactjsreact-hooksreact-routerreact-router-dom

Is it possible to use multiple outlets in a component in React-Router V6


I am using React Router v6 in an application. I have a layout page, which uses an outlet to then show the main content. I would also like to include a title section that changes based on which path has been matched, but I am unsure how to do this.

function MainContent() {
  return (
    <div>
      <div>{TITLE SHOULD GO HERE}</div>
      <div><Outlet /></div>
    </div>
  );
}

function MainApp() {
  return (
    <Router>
      <Routes>
        <Route path="/projects" element={<MainContent />} >
          <Route index element={<ProjectList />} title="Projects" />
          <Route path="create" element={<CreateProject />} title="Create Project" />
        </Route>
      <Routes/>
    </Router>
  );
}

Is something like this possible? Ideally, I would like to have a few other props besides title that I can control in this way, so a good organization system for changes like this would be great.


Solution

  • The most straightforward way would be to move the title prop to the MainContent layout wrapper and wrap each route individually, but you'll lose the nested routing.

    An alternative could be to create a React context to hold a title state and use a wrapper component to set the title.

    const TitleContext = createContext({
      title: "",
      setTitle: () => {}
    });
    
    const useTitle = () => useContext(TitleContext);
    
    const TitleProvider = ({ children }) => {
      const [title, setTitle] = useState("");
      return (
        <TitleContext.Provider value={{ title, setTitle }}>
          {children}
        </TitleContext.Provider>
      );
    };
    

    Wrap the app (or any ancestor component higher than the Routes component) with the provider.

    <TitleProvider>
      <App />
    </TitleProvider>
    

    Update MainContent to access the useTitle hook to get the current title value and render it.

    function MainContent() {
      const { title } = useTitle();
      return (
        <div>
          <h1>{title}</h1>
          <div>
            <Outlet />
          </div>
        </div>
      );
    }
    

    The TitleWrapper component.

    const TitleWrapper = ({ children, title }) => {
      const { setTitle } = useTitle();
    
      useEffect(() => {
        setTitle(title);
      }, [setTitle, title]);
    
      return children;
    };
    

    And update the routed components to be wrapped in a TitleWrapper component, passing the title prop here.

    <Route path="/projects" element={<MainContent />}>
      <Route
        index
        element={
          <TitleWrapper title="Projects">
            <ProjectList />
          </TitleWrapper>
        }
      />
      <Route
        path="create"
        element={
          <TitleWrapper title="Create Project">
            <CreateProject />
          </TitleWrapper>
        }
      />
    </Route>
    

    In this way, MainContent can be thought of as UI common to a set of routes whereas TitleWrapper (you can choose a more fitting name) can be thought of as UI specific to a route.

    Edit is-it-possible-to-use-multiple-outlets-in-a-component-in-react-router-v6

    Update

    I had forgotten about the Outlet component providing its own React Context. This becomes a little more trivial. Thanks @LIIT.

    Example:

    import { useOutletContext } from 'react-router-dom';
    
    const useTitle = (title) => {
      const { setTitle } = useOutletContext();
    
      useEffect(() => {
        setTitle(title);
      }, [setTitle, title]);
    };
    

    ...

    function MainContent() {
      const [title, setTitle] = useState("");
      return (
        <div>
          <h1>{title}</h1>
          <div>
            <Outlet context={{ title, setTitle }} />
          </div>
        </div>
      );
    }
    

    ...

    const CreateProject = ({ title }) => {
      useTitle(title);
    
      return ...;
    };
    

    ...

    <Router>
      <Routes>
        <Route path="/projects" element={<MainContent />}>
          <Route index element={<ProjectList title="Projects" />} />
          <Route
            path="create"
            element={<CreateProject title="Create Project" />}
          />
        </Route>
      </Routes>
    </Router>
    

    Edit is-it-possible-to-use-multiple-outlets-in-a-component-in-react-router-v6 (forked)