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

3

18

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.

Wed answered 10/1, 2022 at 15:49 Comment(1)
It is possible to just have <Routes> instance in each URL-dependent section instead of an <Outlet>Brindabrindell
L
8

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)

Linders answered 10/1, 2022 at 16:20 Comment(6)
Thanks, this is actually the solution I have been using to set the title. I asked this question because I now want to add a second parameter (I have an 'action' section with a few buttons...I want the style/placement of the buttons to be the same, but which buttons should be a route prop). I suppose I could double wrap it in a TitleProvider and ActionProvider, but this seems like its getting messy quick if this keeps expandingWed
@Wed That's sort of why I included the last blurb about how to think about those two components a bit more abstractly. Certainly for separation of concerns using separate React contexts is optimal, but I get the concern about the wrapping. To make the code more DRY you could declare a single PageWrapper component that accesses all the custom context hooks and takes all the props and for them, the result would would look much like the above for the routing. Does this make sense or would you like me to update with a more general example solution?Linders
v6 offers its own hook, called useOutletContext, to share state or content between parents & children : reactrouter.com/docs/en/v6/api#useoutletcontextLarue
+ 1 I built an entire layout system using the context with about 5 sets of setters... titlebarLeftArea, titlebarRightArea, color, etc. It's very powerful if you do it right. It actually means the contained component controls aspects of it's parent layout. Good stuff.Ta
Unfortunately editing context with setState in children results in flickering on page load and ERRORS in SSR: Cannot update a component (A) while rendering a different component (B). If you try to wrap setState call with useEffect you'll get another error: This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransitionDesk
@Desk Seems your issue might be more related to using SSR. I recommend you create a new post for your (possibly related) specific issue. Be sure to include a complete minimal reproducible example and feel free to reference any work based on this post. If you do this, feel free to ping me in another comment here with a link to your post and I can take a look when available. In the least, you'll very likely get more eyes on your problem much faster than hashing it out in comments here with me alone.Linders
C
3

Another solution is to use the handle prop on the route as described in the useMatches documentation.

import { useMatches } from "react-router-dom";

function MainContent() {
  const matches = useMatches()

  const [title] = matches
    .filter((match) => Boolean(match.handle?.title))
    .map((match) => match.handle.title);

  return (
    <div>
      <div>{title}</div>
      <div><Outlet /></div>
    </div>
  );
}

function MainApp() {
  return (
    <Router>
      <Routes>
        <Route path="/projects" element={<MainContent />} >
          <Route index element={<ProjectList />} handle={{ title: "Projects" }} />
          <Route path="create" element={<CreateProject />} handle={{ title: "Create Project" }} />
        </Route>
      <Routes/>
    </Router>
  );
}
Cruikshank answered 17/2, 2023 at 17:6 Comment(1)
IMHO, this is better than bringing in contextDecemvir
B
2

I was facing the same issue for a left-right layout: changing sidebar content and main content, without repeating styling, banner, etc.

The simplest approach I found was to remove nested routing, and create a layout component in which I feed the changing content through properties.

Layout component (stripped for this post):

export function Layout(props) {

  return (
    <>
      <div class="left-sidebar">
        <img id="logo" src={Logo} alt="My logo" />
        {props.left}
      </div>

      <div className='right'>
        <header className="App-header">
          <h1>This is big text!</h1>
        </header>

        <nav>
          <NavLink to="/a">A</NavLink>
          &nbsp;|&nbsp;
          <NavLink to="/b">B</NavLink>
        </nav>

        <main>
          {props.right}
        </main>
      </div>
    </>
  );
}

Usage in react router:

<Route path="myPath" element={
  <Layout left={<p>I'm left</p>}
          right={<p>I'm right</p>} />
} />
Brewton answered 1/4, 2022 at 20:20 Comment(1)
It breaks the abstraction of nested routing, so beware if any of the left or right content areas are now rendering descendent routes that this parent route needs to specify a trailing wildcard "*" matcher to its path. Otherwise, this likely also works in certain use cases.Linders

© 2022 - 2024 — McMap. All rights reserved.