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

Accept objects in push and replace #141

Merged
merged 3 commits into from Dec 1, 2015

Conversation

taion
Copy link
Contributor

@taion taion commented Nov 19, 2015

Discussed on Discord a bit, thought I'd get some code in place.

I think this is a bit of a first step toward remix-run/react-router#2186, as described in remix-run/react-router#2186 (comment).

@taion
Copy link
Contributor Author

taion commented Nov 19, 2015

The immediate benefit of this will be to enable patterns like

history.push({ ...location, query: nextQuery })

@taion taion force-pushed the push-replace-location-object branch from 6eca7c7 to 563b9eb Compare November 19, 2015 02:08
@taion
Copy link
Contributor Author

taion commented Nov 19, 2015

Squashed in a couple of test cases showing off the using spreads.

I'm aware this doesn't update the changelog - it takes me a long time to write that sort of thing, so wanted to get feedback on the new functionality before adding a changelog entry.

@taion
Copy link
Contributor Author

taion commented Nov 19, 2015

Ah - this is a little tricky because of our semantics with handling search and query. I added some failing test cases for the moment.

@taion
Copy link
Contributor Author

taion commented Nov 19, 2015

Well - this resolves it. I don't love this implementation, but I don't hate it either.

For a breaking change, it'd be nicer to just drop the search entirely from the location that listen gives to listeners; this would also make the API a bit cleaner, since it means that the search I get back is exactly the search I pass in.

In practice, though, I think this implementation is good enough.

@mjackson
Copy link
Member

I have a local branch that implements this as well; just haven't had time to push it yet. I'll take a look at this on my flight to the UK this weekend. Thanks for all your work here, @taion. :)

@taion
Copy link
Contributor Author

taion commented Nov 26, 2015

A couple of things to note - I'd like to resolve this TODO sooner rather than later, but it'd require a (mostly non-breaking) 2.x. Should hold off on actually doing a major version bump to aggregate a few more of these technically breaking changes, though.

Also, my real motivation here isn't just that this API feels "cleaner" in some sense. Concrete things:

  • I strongly suspect that supporting mobile routing is going to require a bit more than just the existing state, path, query tuple, and is going to instead need a few extra keys. For this, it's going to be nicer to pass in an object from the start, rather than mucking with the signature on pushState. cc @skevy
  • This lets someone implement named routes as a first-class things w/e.g. a useNamedRoutes history enhancer, that allows the user to do e.g. history.push({name: "foo", params: {fooId: 3}}) (and possibly just history.push("foo")). It wouldn't even require a different <Link>, as <Link> could just accept an object for props.to, and pass that to history.push.

@taion
Copy link
Contributor Author

taion commented Nov 26, 2015

cc @jlongster

This also opens up the path toward adding a more generic history.transition (or history.update or whatever) that doesn't presuppose action. Might be a better entry point for an integration like redux-simple-router that just wants to forward stuff onto the history; otherwise for completeness I feel like that library ought to have a replacePath (with the current API).

@taion
Copy link
Contributor Author

taion commented Nov 26, 2015

On second thought, that might be too generic an API. Probably better for integrations that want to do this to just implement both push and replace hooks.

@mjackson
Copy link
Member

The work required for #162 is going to touch a lot of these same points. I think it may be a bit cleaner to do that first.

@taion
Copy link
Contributor Author

taion commented Nov 30, 2015

I disagree - I don't think #162 is on-path. The longer-term goal here is to offer a "generic" API to push and replace that better supports paradigms outside of the state/query/path paradigm, which is very focused toward doing path-based routing in browser. One of the goals here is to move the React Router <Link> API to be something like:

<Link to={{path: '/foo', query: {the: 'query'}, state: {the: 'state'}}>

Where the arguments to link.props.to are passed directly to history.push. This then lets users just sub out the history without changing the <Link> component, which is going to be really great for RN support, as well as for "good" userspace named routes support.

@ryanflorence
Copy link
Member

Where the arguments to link.props.to are passed directly to history.push

This is appealing.

if (location.query == null) {
const { search } = location
location.query = parseQueryString(search.substring(1))
location[SEARCH_BASE_KEY] = { search, searchBase: '' }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've read the code through a few times, but I'm still not sure what searchBase is or why it's necessary. Is there any way you can make this code more clear?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for https://github.com/rackt/history/pull/141/files#diff-822f74db4c614a797843213f3d8ad044R71.

Suppose you do history.push({...location, query: nextQuery}). Because useQueries currently appends the queries to the existing search, this will append the contents of the query to the old search.

I don't expect this to be what anyone wants. The ideal solution here is to change the useQueries semantics to overwrite search entirely, but I don't want to make a breaking change yet.

This is a bit of a hacky workaround - just check to see what the "intended" search is. It's ugly, but I couldn't figure out anything better that was non-breaking. I think this will work well enough for most practical use cases, and would rather ship this than hold off on a 2.x release for this change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the test case you linked to, the given query overwrites the pre-existing query string instead of appending to it. Am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of this logic is exactly to allow that overwriting (not exactly overwriting but close).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The $searchBase is quite a bit of a hack. The way this works is that if you have a user-pushed location that looks like e.g.

{
  path: '/the/path',
  search: '?a=one',
  query: { the: 'query value' },
}

We end up getting producing something that looks like:

{
  path: '/the/path',
  search: '?a=one&the=query+value',
  query: { the: 'query value' },
  '$searchBase': {
    searchBase: '?a=one',
    search: '?a=one&the=query+value',
  }
}

Suppose we want to follow the pattern above and do something like:

history.push({
  ...location,
  query: { other: 'query value' },
})

The object that actually gets pushed looks like:

{
  path: '/the/path',
  search: '?a=one&the=query+value',
  '$searchBase': {
    searchBase: '?a=one',
    search: '?a=one&the=query+value',
  }
  query: { other: 'query value' },
}

Since the search matches history['$searchBase'].search, we replace it with history['$searchBase'].searchBase, and in a roundabout way end up with search as ?a=one&other=query+value, which (given the current append semantics) is what we expect.

On the other hand, if the user does something like:

history.push({
  ...location,
  search: '?b=two',
  query: { other: 'query value' },
})

Then we end up with search as ?b=two&other=query+value.

There are edge cases here, but I expect that people are very unlikely to run into them in practice. I intend this as a hack that we remove ASAP, but one that is necessary if we don't want to break backward compat per the current "when there is already an existing search" test case.

The idea would be something like releasing this as v1.14.0 and taking a bit more time to gather stuff like the potential query-string for qs replacement before cutting v2.x. The TODO below is sort of in lieu of further documentation, because I didn't want to imply that this was deservedly complicated code that we should keep - it isn't anything of the sort.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks for the detailed explanation.

I think ultimately useQueries should probably just ignore any existing search value and we should alter that test to make sure the existing query string is overwritten. I think this is a lot clearer in the object-based API where everything is in the same argument. I understand you'd like to keep backwards compat for now, but let's def drop in 2.x.

mjackson added a commit that referenced this pull request Dec 1, 2015
@mjackson mjackson merged commit ce96689 into remix-run:master Dec 1, 2015
@mjackson
Copy link
Member

mjackson commented Dec 1, 2015

Thanks, @taion! Let's be sure to follow up with a note in CHANGES.md.

@taion
Copy link
Contributor Author

taion commented Dec 1, 2015

Ah crud, I should also have cleaned up the commits on this branch.

I want to drop the hack in useQueries ASAP - entirely just a question of what else we want to break first. I'm trying to track possible things on #164... possibly at least all the things under "minor", just to get those out of the way.

@taion taion deleted the push-replace-location-object branch December 1, 2015 12:30
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants