Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v6 beta, cant pass props through Outlet. #7495

Closed
moeatsy opened this issue Jul 9, 2020 · 22 comments
Closed

v6 beta, cant pass props through Outlet. #7495

moeatsy opened this issue Jul 9, 2020 · 22 comments

Comments

@moeatsy
Copy link

moeatsy commented Jul 9, 2020

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


@timdorr
Copy link
Member

timdorr commented Jul 9, 2020

No, because you can pass them via URL parameters.

@timdorr timdorr closed this as completed Jul 9, 2020
@kombuchafox
Copy link

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

@MeiKatz
Copy link
Contributor

MeiKatz commented Nov 25, 2020

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>

@remix-run remix-run deleted a comment from seelojurajesh Dec 9, 2020
@remix-run remix-run deleted a comment from seelojurajesh Dec 9, 2020
@cereallarceny
Copy link

cereallarceny commented Dec 10, 2020

@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: #7590

@MeiKatz
Copy link
Contributor

MeiKatz commented Dec 10, 2020

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

@eakl
Copy link

eakl commented Jul 15, 2021

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

@rwieruch
Copy link
Contributor

rwieruch commented Nov 7, 2021

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.

@bradwestfall
Copy link
Contributor

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)

@rwieruch
Copy link
Contributor

Thanks @bradwestfall for providing a work around! I hope this will become native in RR6 though :)

@kombuchafox
Copy link

@bradwestfall nice

@MeiKatz
Copy link
Contributor

MeiKatz commented Nov 12, 2021

For consistency (with the context API) I would call the prop value instead of data but that belongs to the one who's using it.

Btw: exactly the solution I was thinking about 👍

@mjackson
Copy link
Member

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();
  // ...
}

@mjackson mjackson reopened this Nov 13, 2021
@mikel-codes
Copy link

@bradwestfall nice and Thanks

BUT::

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
}

assuming this in a file called Outlet.js
as per this when I do this

import Outlet from "./Outlet"
function ComponentFunc(){
     return <Outlet data="some data' />
}

How do I then import or use the useOutletContext Part?

@arinthros
Copy link
Contributor

Just throwing out a potential alternative solution using React.cloneElement to pass any props to the route element. It's a lot less cognitive load for whoever is using it.

I'm happy to make a PR for this pattern if it's a direction you support @mjackson @timdorr.

// React Router
function Outlet(props) {
  // routeElement comes from whatever internals are already used
  return React.cloneElement(routeElement, props)
}

function AppRoute() {
  return (
    <Route path="user" element={<User />}>
      <Route path="profile" element={<UserProfile />} />
    </Route>
  )
}

function User() {
  return (
    <Outlet prop1="some string" prop2={{some: 'object'}} />
  )
}

function UserProfile({prop1, prop2}) {
  // return stuff
}

@bradwestfall
Copy link
Contributor

@mikel-codes You would just import useOutletContext and call it in components that are rendered from

@arinthros I tried that too but it doesn't work with the API's we're given. We can call React Router's useOutlet like I did to get an element but that element is an instance of an internal provider that wraps your actual "page" element. It's not something that when cloned will lead do your prop1 and prop2 getting passed down.

There's probably an internal way for React Router to do it internally, but I don't think that's the approach they want. I don't want to speak for Michael and Ryan directly, but based on conversations Ive had with them and seen them have with others, there's drawbacks to that approach. I don't think it's a lot less cognitive noise either. From a developer's standpoint, one approach means receiving props and the other means calling a hook, that's the only real difference

@timdorr
Copy link
Member

timdorr commented Dec 10, 2021

This is now possible with <Outlet context> in 6.1.0

@timdorr timdorr closed this as completed Dec 10, 2021
@rwieruch
Copy link
Contributor

@timdorr thanks for making this happen! Is there any documentation already there which we can read up?

@JotarosStarPlatinum
Copy link

@rwieruch https://reactrouter.com/docs/en/v6/api#useoutletcontext

@umarjavedse
Copy link

umarjavedse commented Dec 15, 2021

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

@SammyJoeOsborne
Copy link

SammyJoeOsborne commented Mar 16, 2023

Also worth mentioning since I didnt see it in the useOutlet documentation, and I think @mjackson touched on this with the <Outlet> component, but if you're using the useOutlet hook, it can accept any optional data as an argument. Whatever you pass is made available via useOutletContext in the child component.

const currentOutlet = useOutlet(whateverYouWant)
return (
  <div>{currentOutlet}</div>
);

@SKOLZ
Copy link

SKOLZ commented Jun 6, 2023

Quick question, if my sub routes expect different props from the parent route, what's the best way of passing what they specifically need? should I send all the props through the context and let the consumer sub routes pick what they need or is there any other alternative I'm not considering? thanks in advance

@kiliman
Copy link
Contributor

kiliman commented Jun 6, 2023

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests