faceyspacey / redux-first-router

🎖 seamless redux-first routing -- just dispatch actions
MIT License
1.57k stars 143 forks source link

Allow accessing custom history state #133

Closed luisherranz closed 6 years ago

luisherranz commented 6 years ago

I need to be able to store custom state on each route because the "pretty" URL on our app doesn't have all the info we need. Something like this:

history.push('some-pretty-url', { type: 'post', id: 435, filters: { cat: [3, 16] } });

The history package allows this as a second parameter:

history.push(path, [state]);

And the HTML5 spec as the first one:

history.pushState(stateObj, "page 2", "bar.html");

If you want to add support, maybe similar API to the query one you just did would work fine: https://github.com/faceyspacey/redux-first-router/blob/master/docs/query-strings.md

Add action.state or action.meta.state to your action:

<Link to={{ type: 'FOO', payload:  { bar: 1 }, query: { baz: 2 }, state: { bax: 3 }}}>

Then access it on store.getState().location.state. That's it.


BTW I love the initiative. I'm porting a project from next to your libraries right now :)

faceyspacey commented 6 years ago

Glad you like it.

the only thing with the state aspect of the push API is what if the user visits it directly? You can't get that state.

Also why do you have to call push directly? Why not just dispatch an action that triggers the same URL. You can put any additional key/vals on your payload and the middleware won't complain. You just have to have the minimum :params to complete the path.

luisherranz commented 6 years ago

That was fast.

the only thing with the state aspect of the push API is what if the user visits it directly? You can't get that state.

We populate the first state object on the server (SSR) so we got that case covered.

But sometimes, that's not even needed. For example, in nextjs, they use the history state object to show a different url when users visit directly than when users navigate from another page: https://github.com/now-examples/nextgram https://nextgram.now.sh/

So there are different use cases.

Also why do you have to call push directly? Why not just dispatch an action that triggers the same URL. You can put any additional key/vals on your payload and the middleware won't complain. You just have to have the minimum :params to complete the path.

You mean to add that state to our actions and store it outside the router, right? That could work, and we could deal with the browser's back button scenario. It's probably not that hard.

The cool thing about the history's state object is that it's synced with the URL for you. If the user hits the back button you instantly get the previous state object populated. No sync needed.

As both the npm history and HTML5 spec support this case and it looks like really easy to add, I thought I'd ask. I could help with the PR if you are interested :)

faceyspacey commented 6 years ago

i mean if it's just a case of adding the additional args to push/replace, that's easy enough. In fact I just added it. So it will be in the next release of @rudy:

export const push = (pathname: string, state?: any) =>
  _history.push(pathname, state)

export const replace = (pathname: string, state?: any) =>
  _history.replace(pathname, state)

not sure if that 100% gets you what you're after though. That doesn't put it in the location state, and it doesn't handle <Link /> for you. I might leave that bit up to a PR by you.

That said, to do the instagram thing, you can do that by using the kind === 'load' key available in both the actions and location reducer state. So if the kind is "load" render a different background, etc. If this is just for the instagram case, you're covered just by detecting the kind, and you never need the state argument. If it's not load, display it the modal way.

But if there is yet another case, you can solve the links by using actions and adding additional keys to the payload. That wouldn't solve the location reducer, but you can create your own reducers. Let me know if any or all of this has you covered or why else we NEED the our actions to still put the state in the traditional history, plus location reducer??

It's not difficult, it's just API surface which can be confusing to users who don't want to be encumbered by old ways, especially newer developers that have never used it.

luisherranz commented 6 years ago

That said, to do the instagram thing, you can do that by using the kind === 'load' key available in both the actions and location reducer state. So if the kind is "load" render a different background, etc. If this is just for the instagram case, you're covered just by detecting the kind, and you never need the state argument. If it's not load, display it the modal way.

Awesome. Didn't know about that!

It's not difficult, it's just API surface which can be confusing to users who don't want to be encumbered by old ways, especially newer developers that have never used it.

You are right, don't worry. We'll solve it on our end :)

imrekoszo commented 6 years ago

So it will be in the next release of @rudy

Hey, any idea when this will get released?

faceyspacey commented 6 years ago

end of the week or next week hopefully.

imrekoszo commented 6 years ago

Good stuff, thank you. Maybe a stupid question but will the browser location state be available in the location state tree?

faceyspacey commented 6 years ago

I'm pretty sure I'm gonna go that route. There has been some crazy challenges there. The original history package doesn't support being able to keep track of the history entries array--since disabled cookies disables localStorage + sessionStorage, and since private mode in safari also disables it. So that means when someone backs or forwards out of your site, you can't reliably remember the entries.

But I've settled on a solution: for the less than 1% of users this applies to, we'll fall back to using the memory storage, and then hi-jack history.push to both do a memory push + realHistory.replaceState. In terms of the real address bar, it will always be replaced, and there will only ever be one entry in history.entries.

That means in such a rare case where there is no sessionStorage, when the user presses back or forward, it will always leave the site. It will correctly return you to the correct URL if you return to your site via the browser buttons, but you won't be able to navigate back through other entries. Not via the browser buttons.

Internally your application won't know the difference--it will have a complete stack of history entries. This ultimately solves a large number of problems that aren't obvious without getting into various implementation challenges. I think it's the perfect solution ultimately. The cost is small: for those very small percentage of users they can't navigate through your site via the back/next buttons (not without leaving). The percentage some say is as small as .02%.

imrekoszo commented 6 years ago

Hmm I feel quite stupid now for not understanding why you need sessionStorage? Wouldn't window.history provide data that is needed?

faceyspacey commented 6 years ago

it doesn't. for security reasons. they wont tell you the entries. you won't get an array, and a length and an index like you do with memory history. you only get a useless length. The browser also doesn't tell you if you're going back or forward. So you need to maintain your own sessionStorage as a pre-requisite to interpret this stuff. And then, its not guaranteed to work all the time.

imrekoszo commented 6 years ago

Thanks for the explanation. Feel free to ignore me if you had enough questions, but I'll write them here anyway.

I can see not knowing back or forward is a problem, could that not be resolved by keeping a counter and adding it to history.state on every navigation as explained here? Unless I'm missing something again.

What I can't seem to understand is the need to have access to all the entries in the history stack? Is there a common usecase for that?

faceyspacey commented 6 years ago

Check the first comment to that post. SessionStoragenis unstable. It's not available when cookies are off and safari private mode. Those occasions are only 1%, so if u do do it u need a fallback.

I'll be explaining more in the future why we neee all this. It's opened up some interesting possibilities for us that make out new architecture extremely powerful. The clue is that we now have the koa pipeline architecture and are independent of both the browser history lifecycle and the Redux dispatch lifecycle, but we can hook into them precisely when we need, and the user can customize it as well. Think of it like a funnel of events or actions they must take place: blocking route changes, callbacks like beforeEnter and onEnter and onLeave, thunk; dispatching to Redux, changing the address bar, update scroll restoration; whatever u want.

U can now control the order of alll these things in our "low level" koa middleware style API. And of course there is a high level API based on global and route level options, which uses an advanced and feature complete "recipe" for said middleware pipeline.

I've abstracted everything away from the limitations of Redux and history to attain a truly pro architecture.

To do this, we needed control of history entries and knowing exactly where we are after backing out or forwarding out of the site, or refreshing in the middle of the stack.

In the 1% case we fallback to memoryHistory and never add entries. If the browser has push/replaceState we simply update the single entry in the stack. If not, URL never changes. But if it does, the URL will stay in sync but if u hit back u will always go to the previous site. And the inverse with forward. But at least we know our entries and back/next info. Keep in mind that's for a fraction of a percent of users doing advanced things like disabling cookies, and really old browsers that browserHistory wouldn't support in the first place. We in fact support them unlike React router--just in a reduced capability mode.

faceyspacey commented 6 years ago

Basically we revert all pops immediately, and then manually change the URL later when we want. But if it was a back pop or forward pop, ie go, we need to know which to perform. And if it was a push or replace we need to know too. And then u need to know when the user returns to the site, which may be on any index of the entries array. U gotta recall where they will be moving along the entry track from.

Hopefully that's a bit less abstract. I'll show U the code when it's out.

eliseumds commented 6 years ago

Oh, that would be a lovely addition! Our use case is the following:

I imagine using it like that:

function ProductSearchListItem({ product }) {
  const linkTo = {
    type: 'PRODUCT_PAGE',
    payload: { slug: product.slug },
    state: { product }
  };

  return (
    <List.Item>
      <Link to={linkTo}>
        <Media>
          <ProductLogo product={product} />
          <Media.Body>
            <Text>{product.singularShortName}</Text>
            <ProductTags product={product} />
            <ProductPrice product={product} />
          </Media.Body>
        </Media>
      </Link>
    </List.Item>
  ); 
}

This greatly improves the user experience. Check out a live example on Flipkart.com: https://www.flipkart.com/mobile-accessories/power-banks/pr?sid=tyy,4mr

faceyspacey commented 6 years ago

sick. glad to hear you see the value. i think having route level state is gonna be BIG.

you will be able to do connect(mapStateToProps, { setState })(MyComponent)!!!

So you can setState at the route level and access it from any component in the tree. Then when you return to the page, it's still there. In addition you will be able to do setState(state, index), and set state on different indexes as well. And of course you can retrieve.

This is still just the tip of the ice berg of what's cooking.