react-router: v6 beta, cant pass props through Outlet.

My current project’s previous team had used a lot of cloneElement to pass props to nested routes.

{Children.map(children, (child) =>
          cloneElement(child, {
            somefunc: this.somefunc,
          })
        )}

So I got a tons of code to refactor now, and I do not see a way to migrate this code to v6. Do you consider way to allow pass props like <Outlet myProp=1/>, to pass it into underlying route’s component?

I’ll give you an example to be sure

//index.js
<Route path='/' component={<App />} />
   <Route path /1' component={<1 />} />
   <Route path /2' component={<2 />} />
</Route>

//App.js
render() {
 return <Outlet myprop='1' />
}

//1/2.js
this.prop.myprop === 1 // true


About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 12
  • Comments: 22 (13 by maintainers)

Most upvoted comments

No, because you can pass them via URL parameters.

We definitely have plans to allow passing stuff through outlet. Sorry @timdorr, I should’ve let you know before you closed this.

Right now we’re thinking this will look something like:

<Outlet context={whateverYouWant} />

function ChildRoute() {
  let stuff = useOutletContext();
  // ...
}

In case anyone is looking for a nice solution, do this with context:

import React from 'react'
import { useOutlet } from 'react-router-dom'

const Context = React.createContext()

export function Outlet({ data }) {
  const el = useOutlet()
  return <Context.Provider value={data}>{el}</Context.Provider>
}

export function useOutletContext() {
  const context = React.useContext(Context)
  if (!context) {
    throw Error('Using context while not in an Outlet Provider')
  }
  return context
}

Now you’d use <Outlet data={} /> from this code instead of the one from React Router (this one just wraps theirs). Then in the nested page just call useOutletContext()

FWIW, Michael and Ryan are probably going to make something (maybe similar to this) available in RR later (based on some chatter I’m seeing)

@timdorr @MeiKatz Don’t you find this a bit ugly? If I have a series of child routes for a dashboard that all have the same data requirements, wouldn’t it rather convenient to just pass this in once through the <Outlet /> as opposed to passing it through the component in the element prop?

To take another example, consider following routes definition:

<Routes>
  <Route path="/" element={<Homepage />} />
  <UnauthRoute path="signup" element={<Signup />} />
  <UnauthRoute path="signin" element={<Signin />} />
  <Route path="users">
    <Route path="/" element={<Navigate to="/" />} />
    <AuthRoute path="settings" element={<Settings />} />
    <Route path=":uid" element={<Profile />} />
  </Route>
  <Route path="courses" element={<Courses />}>
    <Route path="/" element={<CoursesSearch />} />
    <Route path=":course">
      <Route path="/" element={<CourseOverview />} />
      <AuthRoute path="project">
        <AuthRoute path="/" element={<CourseProject />} />
        <AuthRoute path=":part" element={<ProjectPart />} />
      </AuthRoute>
      <AuthRoute path=":lesson">
        <AuthRoute path="/" element={<CourseLesson />} />
        <AuthRoute path="complete" element={<CourseLessonComplete />} />
        <AuthRoute path=":concept" element={<CourseConcept />} />
      </AuthRoute>
    </Route>
  </Route>
  <Route path="policy" element={<PolicyAndTerms />} />
  <Route path="terms" element={<PolicyAndTerms />} />
  <Route path="*" element={<NoMatch />} />
</Routes>

If you look real quick at all courses/* routes, you’ll notice they’re composed by a parent <Courses /> element where my <Outlet /> statement is. Since all routes under that main courses/* route need to have access to 1. the data of the specific course in question, 2. all other courses available on the website, and 3. the current user’s progress on all various courses, it would be rather inconvenient to pass these props on all the element’s that fall under that tree. Wouldn’t it just be better to pass them as props on the <Outlet /> itself? If we don’t have the ability to do that, I need to make all 3 of these API calls at the top of my routes definition file, even though the data is only relevant for roughly half the routes on my entire site.

Is there some particular reason you wouldn’t want data to be able to be passed as a prop on an <Outlet /> component? Does this greatly increase the complexity of the API you’re offering in v6?

Adding a reference to this other issue with the same question: https://github.com/ReactTraining/react-router/issues/7590

You can pass them to the element in <Route />

<Route path='/' component={<App />} />
   <Route path /1' component={<Comp1 baz="blub" />} />
   <Route path /2' component={<Comp2 foo="bar" />} />
</Route>

This is now possible with <Outlet context> in 6.1.0

Meaning there is no way to pass props from Parent to Child in a declarative routing system? If we can’t pass props from parent to child in such a way, the declarative routing is just for Layouts? I’m trying to have the routing of my whole app is a declarative way. Is it possible?

// routes.ts
export const routes = [
  {
    path: '/*',
    element: <Parent />,
    children: [
      {
        path: 'child1',
        elements: <Child1 />, // Property 'firstName' is missing in type '{}' but required in type 'IProps'
      },
      {
        path: 'child2',
        elements: <Child2 />, // Property 'lastName' is missing in type '{}' but required in type 'IProps'
      }
    ]
  }
]

// Parent.ts
const Parent = () => {
  const firstName = 'John'
  const lastName = 'Doe'

  return (
    <h1>Parent Component</h1>
    <Outlet /> // How to pass the appropriate props?
  )
}

// child1/2.ts
interface IFirstNameProps {
  firstName: string
}

interface ILastNameProps {
  lastName: string
}

export const Child1 = (props: IProps) => {
  return (
    <h2>First Name</h2>
    {props.firstName}
  )
}

export const Child2 = (props: IProps) => {
  return (
    <h2>Last Name</h2>
    {props.lastName}
  )
}

Stumbled across this myself today. So instead of using the Outlet, one is supposed to pass everything through the <Route/>'s rendered element? Curious if there is anything to read up about the thought process behind it and whether it is in discussion to change it.


Use Case:

<Route path="users" element={<Users />}>
  <Route path=":userId" element={<User />} />
</Route>

Users:

export const Users = () => {
  const users = [
    { id: '1', firstName: 'Robin', lastName: 'Wieruch' },
    { id: '2', firstName: 'Sarah', lastName: 'Finnley' },
  ];

  return (
    <>
      <h1>Users</h1>

      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <Link to={`/users/${user.id}`}>
              {user.firstName} {user.lastName}
            </Link>
          </li>
        ))}
      </ul>

      <Outlet users={users} />
    </>
  );
};

User:

export const User = ({ users }) => {
  const { userId } = useParams();

  // get user from users by id
  // but I don't receive users via Outlet
  // so I have to lift users up to App component to pass them to User component

  return (
    <>
      <h1>User: {userId}</h1>

      <p>Protected Page</p>
    </>
  );
};

I think the concept of Descendant Routes would help me here, however, I would love to declare all routes at the top and use the Outlet as a tunnel instead.

@SKOLZ Take a look at useMatches. It lets you access route data from all active routes.

const rootData = useMatches()[0].data
const leafData = useMatches().at(-1).data

https://reactrouter.com/en/main/hooks/use-matches

@timdorr thanks for update but <Outlet context /> still not working getting undefined, I’ve updated to v6.1.1. Added details in #8492

In this case: why don’t you use a custom context?

Can we reconsider this ? There is certain props I do not want to pass via URL parameters