React-router: [Discussion] V4 <Match>

Created on 14 Sep 2016  Â·  13Comments  Â·  Source: ReactTraining/react-router

Hi!

disclaimer: this is not an issue but an idea that I want to bounce with you guys, so prioritize accordingly.

I've been looking at V4 and I love it. I been thinking for a while that the router could help flux / redux to be more decoupled from the rest of the app by allowing the Route Handlers to receive custom props. The old way of defining routes was a bit in the way for doing this but I think that <Match> is a great candidate.

I don't think it's a super big change, we basically need to have Match accept any props and just use the ones that belong to it's API (pattern, exactly, location, component, etc) and send the rest to the Component that's the Route Handler.

Why this is useful? Imagine that we have an app that's basically a CRUD for puppies.

Suppose we have:

  • <Match pattern={'/pup/:name'} component={Puppy} />: displays the Name and Image of the puppy.
  • <Match pattern={'/pup/:name/edit'} component={EditPuppy} />: enables the user to edit that puppy. (Child route of the above)

And also suppose we are using Redux.

Now, how would you get the puppy's data when you navigate to those routes? You would simply do mapStateToProps and get it from the app's state (by using the :nameroute param)

Now the important part is that both components will be requesting the same data to the state but we are missing the chance to pass the puppy already calculated in <Puppy> to

I think this feature will be really nice for big apps that use a lot of data all around. And the most important aspect is that we almost remove any limitations the Router can impose into app design.

Bonus:
If we could achieve that with the router then I strongly believe that React + React-Router is suitable for small to medium size apps (without Redux or any state management lib) because we can basically have a top level Component that beholds the App State in it's this.state attribute, and instead of actions we would have simple callbacks that in the end will call this.setState for that parent component.

Example:

class Puppy extends Component {
  render() {
    const { puppyName } = this.props.params;

    // You get this through Redux by mapping `puppyName` to a `puppy` object
    const { puppy } = this.props;

    return (
      <div>
        <h1>{puppy.name}</h1>
        <img src={puppy.img} />


        <Match pattern="/pup/:name/edit" component={EditPuppy} puppy={puppy}/>
      </div>
    );
  }
}


function EditPuppy({puppy}) {
  // The actual content of the EditPuppt is not important, but
  // you can assume that we need the puppy object to fill the initial state
  // of the form
  return <PuppyForm puppy={puppy} />
}

I hope my idea is understandable and that I did explain it ok, otherwise let me know and I will work on it.

Thanks a lot for your time
Fran

Most helpful comment

@ryanflorence's proxy is missing passing your match parameters to Match:

const ProxyPropsMatch = ({ component:Comp, ...proxied}) => (
  <Match {...proxied} render={(props) => <Comp {...proxied} {...props} /> } />
)

Or if you don't want to shower the Match with all your unrelated props:

const ProxyPropsMatch = ({ component:Comp, pattern, exactly, location, ...proxied}) => (
  <Match {...{pattern, exactly, location}} render={(props) => <Comp {...proxied} {...props} /> } />
)

This illustrates to me why you don't want Match to do this by default: risk of prop name collisions. A safer way would be something like:

const MatchWithProps = ({ component:Comp, passProps, ...props}) => (
  <Match {...props} render={(matchedProps) => <Comp {...passProps} {...matchedProps} /> } />
)

<MatchWithProps pattern="/foo" component={Foo} passProps={{ bar: 1 }} />

So much flexibility. :smile:

All 13 comments

You can use the render prop instead:

class Puppy extends Component {
  render() {
    const { puppyName } = this.props.params;

    // You get this through Redux by mapping `puppyName` to a `puppy` object
    const { puppy } = this.props;

    return (
      <div>
        <h1>{puppy.name}</h1>
        <img src={puppy.img} />

        <Match pattern="/pup/:name/edit" render={() => {
               <EditPuppy puppy=[puppy]/>
        }}/>
      </div>
    );
  }
}

It can read that directly from the parent closure.

That's very nice, it gives you a lot of flexibility.

I love the original idea of proxy-ing the props to the Route Handler Component though, it seems more Idiomatic React than a "anonymous" component.

Is it something that you might have in account for the future. I could even help implementing it.

Thanks a lot!
Fran

You can wrap match in a component and proxy it yourself, we aren't in the
way :) It's all component composition.
On Tue, Sep 13, 2016 at 7:52 PM Fran Guijarro [email protected]
wrote:

That's very nice, it gives you a lot of flexibility.

I love the original idea of proxy-ing the props to the Route Handler
Component though, it seems more Idiomatic React than a "anonymous"
component.

Is it something that you might have in account for the future. I could
even help implementing it.

Thanks a lot!

Fran

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/ReactTraining/react-router/issues/3826#issuecomment-246890846,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAGHaOBTMOSQCuaHzb5s_5e8YsmetdV9ks5qp2FqgaJpZM4J8NUz
.

Well, that might be a good idea. Like a third party React-Router plugin that exposes <MatchProxy/>. I will definitively give it some though, I'll let you know if have something show-able soon, just for fun. Thanks a lot you both.

const ProxyPropsMatch = ({ component:Comp, ...proxied}) => (
  <Match render={(props) => <Comp {...proxied} {...props} /> } />
)

I seriously love that idea, and that implementation is beautiful. I think
that the composition one can achieve with that is super powerful.

Beginners tend to struggle a lot with Redux, state management and the
integrations, and I always tell them to think of Redux as just the state of
a react component and think of actions as just callbacks (of course I am
missing details but the illustration es powerful enough). That analogy
becomes stronger if you can actually create a single component that has all
the app state and still use the Router, so basically all of this discussion
is awesome. Ill probably use it in my next internal meetup about React.

Thanks a lot for the brainstorming

On Wed, Sep 14, 2016, 00:02 Ryan Florence [email protected] wrote:

const ProxyPropsMatch = ({ component:Comp, ...proxied}) => (
} />
)

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/ReactTraining/react-router/issues/3826#issuecomment-246892331,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABxEJ2pMbVXXixYSsOjaGdRz0DZ58tbuks5qp2PNgaJpZM4J8NUz
.

I see this issue closed, as it can be solved with stateless wrapper components or using the render prop; but my route component is stateful and I don't want it to lose state when it's unmounted and remounted on each parent update.

I was thinking about this today and couldn't come to a solution yet. I'll think about it some more and post if I can find anything. In the meantime if you can solve this please let me know.

And thanks for the fantastic router!

Hey! Do you have any concrete examples? Becaus, first of all this interests
me and probably the router team and secondly because there might be some
tricks to fix this, such as using redux or any other state container to
store state and alternatively move state to an upper parent.

On Sat, Oct 8, 2016, 05:59 Fatih [email protected] wrote:

I see this issue closed, as it can be solved with stateless wrapper
components or using the render prop; but my route component is stateful and
I don't want it to lose state when it's unmounted and remounted on each
parent update.

I was thinking about this today and couldn't come to a solution yet. I'll
think about it some more and post if I can find anything. In the meantime
if you can solve this please let me know.

And thanks for the fantastic router!

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/ReactTraining/react-router/issues/3826#issuecomment-252412883,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABxEJ8jvl356OpTQZsyXXQ3t3kfjqhkGks5qx1t-gaJpZM4J8NUz
.

My first reaction too was moving state up or outside the component as you suggested. But in my case it's too early to introduce Redux to the app and moving state up to the parent will make components tightly coupled and state changes unnecessarily complex. (Passing handlers down the hierarchy is a pain.)

Only other option I could find without changing the router unmounting behavior was moving the data which I want to pass to the route component to context. This has a downside, as I wanted to pass only relevant parts of the data to each route; this way if a route component needs data, it will have all of it.

I don't know my problem generalizes to the general user; most people will want to fetch data on route mount. We're trying an experimental data fetching model where server data is sent as an atom on each update through a websocket so there's no polling, everything's always fresh and realtime, and there's only a single loading screen on app load.

I think I'll tinker with the router a little bit and try to solve it. If I can, I'll send a PR.

your component won't get unmounted just because it gets "rerendered". if it gets unmounted then you changed locations, and that's just how react works, state doesn't persist mounts.

That's exactly what I thought Ryan.

Could you send code snippets? It's much easier to think with concrete
examples rather than a super generic approach.

Alternatively you can double check that your component is remounting with a
console.log or a debugger

On Mon, Oct 10, 2016, 12:07 Ryan Florence [email protected] wrote:

your component won't get unmounted just because it gets "rerendered". if
it gets unmounted then you changed locations, and that's just how react
works, state doesn't persist mounts.

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/ReactTraining/react-router/issues/3826#issuecomment-252650870,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABxEJ02Dwq9yakT01cNlCdZIeJjmLWeHks5qylScgaJpZM4J8NUz
.

I have an isolated test case, please review.

The parent updates state each second and passes it to child route as prop. Child route also has internal state. It gets cleared and loses focus every second.

This is a possible workaround if you have few routes, as the stateless component is created only once as opposed to everytime it's rendered. But it gets unwieldy if you have a lot of routes as you must create anonymous member variables to wrap each route component.

class App extends Component {
  state = { num: 0 }

  componentDidMount () {
    this.interval = setInterval(() => this.setState(() => ({ num: Math.random() })), 1000)
  }

  componentWillUnmount () {
    clearInterval(this.interval)
  }

  // stateless component wrapper that's created only once
  homeRoute = () => <Home num={this.state.num} />

  render () {
    return (
      <Router>
        <div>
          <Match exactly pattern="/" component={this.homeRoute} />
        </div>
      </Router>
    )
  }
}

What I'd consider a better way to solve this would be something like that:

class App extends Component {
  state = { num: 0 }

  componentDidMount () {
    this.interval = setInterval(() => this.setState(() => ({ num: Math.random() })), 1000)
  }

  componentWillUnmount () {
    clearInterval(this.interval)
  }

  render () {
    return (
      <Router>
        <div>
          { /* propsForComponent object, which may be merged with the usual route props */ }
          <Match exactly pattern="/" component={Home} propsForComponent={{ num: this.state.num }} />
        </div>
      </Router>
    )
  }
}

@ryanflorence's proxy is missing passing your match parameters to Match:

const ProxyPropsMatch = ({ component:Comp, ...proxied}) => (
  <Match {...proxied} render={(props) => <Comp {...proxied} {...props} /> } />
)

Or if you don't want to shower the Match with all your unrelated props:

const ProxyPropsMatch = ({ component:Comp, pattern, exactly, location, ...proxied}) => (
  <Match {...{pattern, exactly, location}} render={(props) => <Comp {...proxied} {...props} /> } />
)

This illustrates to me why you don't want Match to do this by default: risk of prop name collisions. A safer way would be something like:

const MatchWithProps = ({ component:Comp, passProps, ...props}) => (
  <Match {...props} render={(matchedProps) => <Comp {...passProps} {...matchedProps} /> } />
)

<MatchWithProps pattern="/foo" component={Foo} passProps={{ bar: 1 }} />

So much flexibility. :smile:

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sarbbottam picture sarbbottam  Â·  3Comments

ArthurRougier picture ArthurRougier  Â·  3Comments

hgezim picture hgezim  Â·  3Comments

misterwilliam picture misterwilliam  Â·  3Comments

stnwk picture stnwk  Â·  3Comments