Skip to content
This repository has been archived by the owner on Oct 26, 2018. It is now read-only.

Support query params? #95

Closed
kimjoar opened this issue Dec 9, 2015 · 51 comments
Closed

Support query params? #95

kimjoar opened this issue Dec 9, 2015 · 51 comments

Comments

@kimjoar
Copy link
Collaborator

kimjoar commented Dec 9, 2015

This morning I've been playing around with how we could support query params in redux-simple-router (just as a thought experiment).

In history.listen we receive a Location. With pushPath and replacePath we can change the API to accept LocationDescriptors. However, with no way of creating a LocationDescriptor object from a LocationDescriptor string, it's difficult to support query params.

Here's an example of the problem:

pushPath('/foo?a=b')
// store:
{ location: '/foo?a=b' }


pushPath({ pathname: '/foo', query: { a: 'b' } })
// store:
{ location: { pathname: '/foo', query: { a: 'b' } } }


history.push('/foo?a=b')
// store:
{ location: { pathname: '/foo', query: { a: 'b' } } }


<Link to='/foo?a=b'>
// store:
{ location: { pathname: '/foo', query: { a: 'b' } } }

The problem is of course that every time you pushPath you will get the exact LocationDescriptor you pass in into the store, but every time you history.push or <Link> you'll get an object. We then can't reliably support query params. (If you're very strict about always passing in a LocationDescriptorObject you would have query support, but that feels very fragile.)

We can of course use history.createLocation right now, but it's most likely no longer going to be public in history V2.

To support query params we need to be able to create a LocationDescriptor object both from a LocationDescriptor string and from a Location. We could then easily support query params, and we could also deepEqual check them without knowing about query or anything else that's actually part of the LocationDescriptor (which is how we stop cycles, #50). With a createLocationDescriptor (that always returns a LocationDescriptorObject) our code base would actually be simpler, and we would get query support for free.

Okey, so some potential solutions:

Keep the solution we have now

I.e. path and state in store, but change to accepting LocationDescriptor in push and replace. We don't support query params.

Verbatim LocationDescriptor in store

We have to make it very clear that this is either an object or a string. So it's most likely not very usable for people. We "support" query params, iff you're very strict about always passing in a LocationDescriptorObject. Fragile.

Always go through history "before" store

pushPath
-> UPDATE_PATH_INTERNAL action
-> store.subscribe callback
-> history.push
-> history.listen callback
-> UPDATE_PATH action
-> store.subscribe callback where we break cycle

I.e. we don't trigger UPDATE_PATH immediately, but only within the history.listen callback. We would then reliably have a LocationDescriptorObject in the store, and we could therefore reliably support query params.

This is of course a more complex solution.

Conclusion

It was interesting going through the different solutions, but with createLocation going away and no createLocationDescriptor on the horizon I think it's best to just keep our current solution. I don't think it's worth the added complexity.

Maybe someone else has ideas on how to solve this? Or maybe this is another case for keeping history.createLocation public? @jlongster @mjackson @taion @justingreenberg

(Hopefully I haven't overlooked something very obvious 🙈)

@mjackson
Copy link
Member

mjackson commented Dec 9, 2015

I tried to eliminate the createLocation API in remix-run/history#182, but I don't think we can yet. I'm not even sure that we can in the long run. Just feels like something we should be able to do away with if we have good support for paths/location descriptor objects everywhere. But history 2 will have createLocation methods on history instances. :)

@kimjoar
Copy link
Collaborator Author

kimjoar commented Dec 10, 2015

Thanks for the input, @mjackson

@justingreenberg
Copy link
Contributor

With a createLocationDescriptor (that always returns a LocationDescriptorObject) our code base would actually be simpler, and we would get query support for free.

i agree 100% now that we know we're not losing createLocation, i feel like it should be easier to track the history api more closely ie syncing with LocationDescriptor object

@kimjoar
Copy link
Collaborator Author

kimjoar commented Dec 10, 2015

Obvious problem: we don't have access to history in pushPath and replacePath. So fairly sure this requires a bigger change than I initially thought :/

@taion
Copy link
Member

taion commented Dec 10, 2015

IMO the correct solution here is your "always go through history" proposal.

The conversion of location descriptors to locations is properly the responsibility of the history package; this means that you can be confident that things will stay in sync and go through exactly the relevant logic for setting up the "push" settings in history.push, as opposed to trying to re-invent this yourself.

In general it feels wrong to me to store location descriptors at all - they're really argument objects and not meant to be canonical, and it feels weird to me to do the location descriptor -> location conversion more than once (i.e. it should just be done once in history and you should use the result from your listen callback).

@kimjoar
Copy link
Collaborator Author

kimjoar commented Dec 10, 2015

Great input @taion, thanks!

@kimjoar
Copy link
Collaborator Author

kimjoar commented Dec 10, 2015

#104 is actually also a point in favour of always going through history.

@taion
Copy link
Member

taion commented Dec 10, 2015

I think you should actually round-trip through the router, FWIW. The location corresponding to the current routing state is not necessarily the history's location - if you use async transitions, the Router location updates after the history one does.

@idolize
Copy link

idolize commented Dec 18, 2015

So, this would add support for getting query params from the router state in the redux store, but what about path params?

Ex: getting the id from a /inbox/messages/:id path

I know this is easy to do inside the React view itself with react-router, but I personally have use cases where I want to do things in my Redux reducers based on this information (without having to put logic in my view layer to dispatch actions based on changing react-router props - this seems completely backwards to me).

Is the recommended approach for this to write helper methods to parse the full pathname string and derive the path params by hand? If so, this seems like a very brittle and time-consuming solution as you would essentially have to write a parse method for each possible path param based on a knowledge of the route hierarchy. If the route configuration changes then you'd have to update each of these helper methods as well.

Why can't the state match the lovely props we get in react-router route components?

@jlongster
Copy link
Member

I want to do things in my Redux reducers based on this information

If we stored more router state in the app state this still wouldn't help you. You don't have access to any other state in your reducer. It sounds like you want it in the UPDATE_PATH action.

Why can't the state match the lovely props we get in react-router route components?

Because props has all sorts of things which do not belong in the app state; things that are unserializable and not dumb JavaScript objects.

@idolize
Copy link

idolize commented Dec 18, 2015

@jlongster Sure, if the UPDATE_PATH action is always guaranteed to fire when the URL changes, and it contains all the relevant information, then I can do the bookkeeping myself. It would in fact be possible for another reducer to read the state of the routing reducer if needed (just call the second reducer with the result of the routing reducer), but you are probably right that it would be preferable for me to manage that myself.

I'm not suggesting one should store every (unserializable) prop from react-router, but the params and query Objects would be extremely useful (in addition to the path String).

@ngthorg
Copy link

ngthorg commented Dec 18, 2015

i am have a example using params. But it (redux-simple-router) not support

function mapStateToProps(state) {
  const { login } = state.router.params

  return {
    user: users[login]
  }
}

export default connect(mapStateToProps)(UserPage)

link example: https://github.com/rackt/redux/blob/master/examples/real-world/containers/UserPage.js#L76

@taion
Copy link
Member

taion commented Dec 18, 2015

Yeah I think especially once we move to the new API, the right place to put this integration is going to be "below" the <Router> in the routing context bit, rather than wrapping the history - solves some async issues and gives you access to the route match, which you can't get by listening to the history.

@ngthorg
Copy link

ngthorg commented Dec 18, 2015

@taion thanks!

@kimjoar
Copy link
Collaborator Author

kimjoar commented Dec 18, 2015

@ngthorg What about something like this:

function mapStateToProps(state, props) {
  const { login } = props.params

The second param to mapStateToProps is the props, so you can get a hold of the params if you want.

@kimjoar
Copy link
Collaborator Author

kimjoar commented Dec 18, 2015

@taion New api? Got an issue or something that I could check out? Haven't had time to stay up-to-date with React Router and History lately.

@taion
Copy link
Member

taion commented Dec 18, 2015

Just remix-run/react-router#2646.

@jlongster
Copy link
Member

@taion neat, would love some help writing code when react-router updates. This library is pretty small and it would be nice to see what your ideal integration is. I understand that we're all busy though.

@taion
Copy link
Member

taion commented Dec 18, 2015

@jlongster Something like... put the thing that listens to the router state under the <Router> component, and just use the standard component lifecycle hooks to pick up router state updates.

@jlongster
Copy link
Member

We also need to trigger route updates though when redux state changes the routing state. I haven't studied the latest react-router changes so I don't know how that's going to work yet, but I'm totally fine with rewriting this if there's something better!

@taion
Copy link
Member

taion commented Dec 18, 2015

That end can just be history.push or history.replace though - even easier (I think) because those now take location descriptors.

@ryancole
Copy link

So, without the URL params in the redux store, like in redux-router, how do you currently handle the situation where you'd like to connect, and select something from the store based on a URL param (/foo/:id)? With redux-router, you can access that URL param right there within the select logic. I'm looking to switch to redux-simple-router, but this is the last remaining question that I have before I can switch.

@taion
Copy link
Member

taion commented Dec 22, 2015

The asymmetry if you support params on the read side (but not the write side - you can't, without named routes) is annoying.

@Dattaya
Copy link
Contributor

Dattaya commented Dec 22, 2015

@ryancole, your component's props should contain params object if you're using react-router:

@connect((state, ownProps) => ({
    product: selectProduct(ownProps.params.productId, state),
    ...
  }),
  actionCreators
)

@kimjoar
Copy link
Collaborator Author

kimjoar commented Dec 22, 2015

That's how I do it. We should put something like this in the docs.

@ryancole
Copy link

I had no idea that props was passed to connect, that's awesome. Time to make the switch!

@ezekielchentnik
Copy link

It would be nice to have access to params without needing to use react-router, in those cases where you still want to stuff routing in your store, but don't actually need full routing capabilities of react-router.

@kimjoar
Copy link
Collaborator Author

kimjoar commented Dec 22, 2015

@ezekielchentnik I think it's likely we will become more coupled to react-router, not less. Also, the tagline has always been:

Let react-router do all the work

@idolize
Copy link

idolize commented Dec 22, 2015

@kjbekkelund Sure, that's fine to be coupled to react-router, and to leverage it internally, but we can't use react-router in redux directly... so if you want to update some redux state based on react-router props it isn't possible unless that data is included in the UPDATE_PATH action.

Using @Dattaya's solution is only feasible when you want to directly render something based on the router state, not when you want to update redux state (ex: "selectedMessage") based on router state (ex: the :id URL param in a /inbox/messages/:id route).

@kimjoar
Copy link
Collaborator Author

kimjoar commented Dec 22, 2015

It wasn't a good answer on my part, sorry. I'll try to add some context around the current state of redux-simple-router:

Right now we only rely on history and redux. We don't know anything about either params or even react-router itself. We know about the URL and state in the browser, and the URL and state in the store — and we try to keep those in sync.

Therefore, getting params into the state requires huge changes. It's the same with history's LocationDescriptor. Both basically require a "rewrite". We might end up doing it, but right now I wouldn't expect it to happen in the very near future, as we haven't started looking much at it. But as you see from the discussion above, we might end up exploring positioning redux-simple-router "below" the <Router> in the routing context bit, rather than wrapping the history after react-router 2 is released. This would give us access to the matching route, and would maybe open up some interesting possibilities. But there's still a lot of thinking and testing needed to get there.

It would be awesome if someone played around with the ideas mentioned in this issue to see if there are nice solutions to the problem. We'd love to see a simple and clean solution that solves this.

@idolize
Copy link

idolize commented Dec 22, 2015

@kjbekkelund Got it. Thanks for the explanation!

@ezekielchentnik
Copy link

@kjbekkelund thanks for the info, makes sense. I'll take a stab, see what I can cook up.

@akrawchyk
Copy link

Is there a workaround to use query params currently? Or should I use route params instead?

@mjrussell
Copy link

@akrawchyk If you are using react-router then you can get the query params from the props passed to the component. This is how works, regardless of wether or not you use redux-simple-router.

In your component:
this.props.location.query.myParam

@taion
Copy link
Member

taion commented Dec 27, 2015

You'd get something like this for close to free with #141 (with an appropriate API change to accept a location descriptor in your action creators) - just push through a location descriptor, and let the whole thing round-trip through history before it hits your store.

@akorchev
Copy link

So far the only way to use route params in a reducer seems to be an extra dispatch from a React component:

componentDidMount() {
    const { dispatch, params } = this.props

    dispatch(selectParam(params.param))
}

To be honest 'redux-router' doesn't support this either because it doesn't expose the action type that corresponds to route changes. One may hardcode it in a reducer but it feels dirty.

@taion
Copy link
Member

taion commented Dec 27, 2015

@akorchev Query params don't go through the router at all. You're thinking router params.

@akorchev
Copy link

@taion yep. Ideally I should get them in my reducer when UPDATE_PATH happens.

@taion
Copy link
Member

taion commented Dec 27, 2015

As discussed elsewhere, that might not strictly be possible - as it stands, history location updates are synchronous, while route matching is not necessarily thus.

@akorchev
Copy link

@taion but it works in 'redux-router' - the only issue there is that they don't expose the action type corresponding to a route change - I have a router object available in the store with everything I could ever need.

@ezekielchentnik
Copy link

here's my stab at using location descriptors, and supporting query params, rely more heavily on history api: https://gist.github.com/ezekielchentnik/261fef0f2f892ffce533

I shall create a PR

@mindjuice
Copy link
Contributor

There are a lot of comments above about the various details of how some parts of this would or wouldn't work, but I'm not sure I see any description of how someone would actually use this. What sort of API would be exposed?

In my current app, I've done the query param synchronization in a very hardcoded way, and am looking to make it more generic. Essentially, there are two cases (in my app).

  1. Whenever I get a willTransitionTo() (sorry old react-router version), I compare the query params to an object I extract from my state. If they differ, I update the state. This handles the user loading a bookmarked URL, typing a URL or forward/back, etc.
  2. Whenever an action fires that changes state, I rebuild the query params from the state and if they have changed, I do transitionTo() with the new query params.

Checking if the query params have changed allows it to avoid infinite loops.

Is the intention to have query params be stored in some separate part of the redux store or to allow me to expose any app state as a query param? (e.g., in my app I have a sortOrder field in the store that can be asc or desc. I want to also have a /sortOrder=asc query param to match it and have them stay in sync automatically.

I can see making these two aspects separate modules, where redux-simple-router gives you the basic ability to get/set query params, then another module gives you the ability to extract state into query params and set query params into app state.

Thoughts?

@mjrussell
Copy link

@mindjuice The master version (not yet officially released) already supports query params (different from React-Router's router params) being stored in the redux-state because they now store the entire location descriptor from history. You can try it with npm install --save redux-simple-router@next. Then its just state.routing.query.sortOrder in the connect function

@akorchev
Copy link

akorchev commented Jan 7, 2016

My use case is very simple. Get a parameter and put it in the store. Say I have an array of items that are displayed in a component. I want to display the details of an item in a separate component which is in another route. Normally I would go to /itemDetails/itemId. All I want is to put the current item in the store so I can then use it from @connect.

switch (action.type) {
   case UPDATE_PATH:
        return state.items.find(item => item.itemId == state.route.params.itemId)
}

@mjrussell
Copy link

@akorchev Its probably worth a new issue if you want to discuss react-router params which you are talking about with /itemDetails/itemId because this is specifically about query params which are part of the history API.

As for your specific case...I would offer an alternative approach. Your state update would not work with combineReducers from redux because you only have access to the local "sliced" state. Instead, you can move the "linking" of the itemId to the item you want inside the connect function.

Something like (es6 with decorators syntax):

import React, { PropTypes, Component } from 'react';
import { connect } from 'react-redux';

function mapStateToProps(state, ownProps) {
   return {
         // routeParams is passed down via props by React-Router
         selectedItem: state.items.find(item => item.itemId === ownProps.routeParams.itemId)
   };
}

@connect(mapStateToProps)
export default class ItemComponent extends Component {
    static propTypes = {
        selectedItem: PropTypes.object,
    }

    render() {
        ....
    }
}

Hit me up on Discord (or ask in the redux-router channel) if you have problems..I think further discussion is outside the scope of this issue

@akorchev
Copy link

akorchev commented Jan 7, 2016

@mjrussell that will work nicely! Thanks. Will it work with hot-reloading?

@timdorr
Copy link
Member

timdorr commented Jan 14, 2016

This is now fixed in 2.0 🎉

@timdorr timdorr closed this as completed Jan 14, 2016
@mindjuice
Copy link
Contributor

@mjrussell What I want to avoid is having my selectors and reducers get any of their dependencies from state.routing.query. I don't think selectors and reducers should know or care whether a given piece of state is part of the route.

I was thinking of having a couple of functions like mapStateToQuery and mapQueryToState to convert back and forth.

If you're using reselect, then mapStateToQuery could just be a selector that extracts the necessary bits and pieces and builds a query object that you store in state.routing.query. Going the other way, mapQueryToState, would require writing a function to update the state from the data in the query params. You would call mapQueryToState when UPDATE_LOCATION is triggered.

Still forming these thoughts, so they may be half-baked. 😄

@mjrussell
Copy link

@mindjuice I agree that you might want selectors to help you pull data out the state.routing.query, but storing the query data inside the store (state) is already done with v2 of this repo because they now store the entire location object. Have you had a chance to check out the newest version? If you want to update the store and url you just dispatch pushState({ pathname: '/foo/bar', query: { objectId: 1 }}) or some alternative history method

@mindjuice
Copy link
Contributor

@mjrussell I haven't used v2 yet, but I understand that it includes the full location object now and that query params are part of that.

The issue for me is that routing data has to be in a specific location in the store. Sub-reducers and selectors in a given module don't get access to the whole store state, so they may not have access to state.routing.query if all I pass in is state.users, for example.

I think the model should be that the reducers update the store state as necessary, but shouldn't care about the router query params at all. I think that should be a separate responsibility.

I feel a bit like we're coming at this with different assumptions and maybe missing each other in the middle.

@mjrussell
Copy link

@mindjuice ok, I can see how that could be difficult with your configuration. Its not clear to me yet what you are proposing as a change, although I do have some potential ways to tweak your config as alternatives.

  1. Listen to the UPDATE_LOCATION type in your state.users reducer and make the necessary updates there to the reducer data. You might even then not even need to include the routingReducer at all if you don't want to. Its just there for convenience.
  2. Don't use combineReducers from redux. Its an awesome function, and useful in tons of places but you don't always have to use it. A reducer is a just a function from state -> action -> state and as long as you obey the rules about no side effects and immutability in your reducers then you can compose & slice them however you want.

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

No branches or pull requests