react-router-scroll icon indicating copy to clipboard operation
react-router-scroll copied to clipboard

Does it support react-router v4 ?

Open suhaotian opened this issue 8 years ago • 23 comments

suhaotian avatar Nov 24 '16 01:11 suhaotian

No. RRv4 has no concept of router middlewares.

taion avatar Nov 24 '16 01:11 taion

Are there plans for something like this to be built for v4?

ntucker avatar Mar 11 '17 18:03 ntucker

I do not plan to do so, no.

taion avatar Mar 11 '17 19:03 taion

I have forked this repo to support React Router v4. The result is here : https://github.com/ytase/react-router-scroll

Minor adjustements were required (like saving the data in sessionStorage instead of the state of history).

I added some doc in the Readme and warning. The library seems to be working as expected, but I haven't taken time to fix the tests yet.

Anybody is obviously free to use my fork to have that great library working under React Router 4, and help bringing something more solid into life (fixing tests, maybe changing some other things in the code etc.).

I can also create a pull request if you are interested, though it is still pretty early for that.

mijamo avatar Apr 05 '17 12:04 mijamo

@mijamo I'm curious why you needed to change the storage backend.

ntucker avatar Apr 05 '17 16:04 ntucker

As you can see here https://github.com/taion/react-router-scroll/blob/master/src/StateStorage.js the storage backend was using DOMStateStorage from the historypackage, which was using sessionStorage under the hood (https://github.com/ReactTraining/history/blob/v3/modules/DOMStateStorage.js for reference).

React Router v4 uses the new version of history that does not have that module anymore. As I needed some very quick fix and didn't have much time I just used sessionStorage straight away. Another possibility would have been to copy that file from the old version of history into the repo, but my opinion was that handling storage of information is probably a separate concern and thus should be handled by something external. The current storage backend is merely a temporary fix that works in most of the cases, and should be replaced by something proper from another library instead of just bringing in more complexity.

Maybe a next step would be to use something like https://github.com/nbubna/store which is widely used for that kind of problem.

mijamo avatar Apr 05 '17 18:04 mijamo

Isn't that implementation going to do weird things when there are redirects?

taion avatar Apr 05 '17 18:04 taion

I have not tried yet, I do not use redirects with react router. As I said it is very early, I just wanted to share my early experiment in case anybody is interested. For now it is working good when I test it but there are tons of improvements possibles and that would be needed to have something ready to be published and widely used.

What is your intuitive thought regarding redirects and in which situation would they cause trouble?

mijamo avatar Apr 05 '17 18:04 mijamo

RRv4 re-renders the page on a redirect, right? so it will try to scroll the page. also render a blank page (ish) and mess up any saved scroll position

taion avatar Apr 05 '17 20:04 taion

I tried quickly with a Redirect and did not encounter the problem you are suggesting. When finding a Redirect, react router just does a history.replace.

So let's say you are at /with key abc, you click a link to /page1, the new location has pathname /page1and key def and you are redirected to /page2 so the new location has pathname /page2 with key ghi but the history is actually abc -> ghi so if you hit previous you will go directly to abcwithout passing by def so it will restore the scroll from abc without any interference from the intermediate redirection. Same if you then hit next, it will just restore ghi.

Or have I misunderstood what you meant?

mijamo avatar Apr 06 '17 07:04 mijamo

The issue I'm thinking of is that, if you click on a link that redirects you, the scroll update will fire twice – once on the page that renders the redirect, then once on the page you actually want to get to. It may well not leave anything broken, but I think it could look janky, especially if the scroll destination is a hash anchor or partway down the page or something.

taion avatar Apr 08 '17 19:04 taion

Unfortunately history.listen for theaddTransitionHook has a major problem. It fails to meet this criterion:

The transition hook function should be called immediately before a transition updates the page

The location.key used to save in sessionStorage is the wrong key. It's the key of the following page since the new state has already been pushed on to the globalHistory object

I.e. this:

https://github.com/ReactTraining/history/blob/master/modules/createBrowserHistory.js#L172

is called before this:

https://github.com/ReactTraining/history/blob/master/modules/createBrowserHistory.js#L183 which notifies listeners.

It's the same for replaceState, pop events, etc.

So that means when you return to the page and the<ScrollContainer /> component mounts, they will attempt to reload their scroll position using the correct location.key and it won't work.

To be clear, I'm talking about the lesser used component for when you want scrollable elements, not the window.

However, the patch here (affecting window) may have broken as well: https://github.com/taion/scroll-behavior/blob/master/src/index.js#L48

But I've yet to have that issue so I can't test it.

ps. It's the reason the original react-router-scroll used router.listenBefore, whish isn't available in the plain history package.

faceyspacey avatar May 02 '17 12:05 faceyspacey

A hacky solution I've been playing with is keeping a record of the previous location.key, and using that for <ScrollContainer /> saves instead.

faceyspacey avatar May 02 '17 12:05 faceyspacey

@faceyspacey You seem to know react-router-scroll internals. I myself am not migrating to react-router@4 yet but in some future that's likely to happen (say, in 2018). So if you happen to have any thoughts which can benefit future seekers make sure you leave them here. I personally didn't understand anything from your comments because they're intended for someone familiar with the inner workings of react-router-scroll.

catamphetamine avatar May 09 '17 14:05 catamphetamine

@mijamo working on upgrading Gatsby to use RRv4 and dropped your fork in and after some basic testing seems to be working great! Will be testing more and contributing back if needs be. Perhaps we should turn your fork into a real project? Or @taion would you be interested in letting us use this repo for the v4 version of this module?

KyleAMathews avatar May 09 '17 17:05 KyleAMathews

@halt-hammerzeit will do.

@KyleAMathews basically, the issues mentioned here should probably be fixed before it becomes its own repo. I actually made a repo for a reworking of this for redux-first-router:

https://github.com/faceyspacey/redux-first-router-restore-scroll

I fixed 2 of the 3 issues, but can't replicate the one i mentioned above. ..and ultimately 1 of the issues doesn't apply to redux-first-router (redirects on rendering). So I only fixed one.

The hard part really is React Router will need to expose a listenBefore hook of its own. It won't be able to come from the history package. So you should ask the maintainers at React Router (or provide a PR) for this. Bring it up in the forum over there, they'll know what you're talking about because the old router did have this method.

faceyspacey avatar May 10 '17 02:05 faceyspacey

I'd be okay with merging the RRv4 bits, assuming the outstanding issues are resolved.

taion avatar May 10 '17 18:05 taion

What's that status of this? Should folks move to the fork?

esprehn avatar Oct 12 '17 00:10 esprehn

If it's working for everyone, we can merge it, I guess. I'd rather the issues here be addressed, but probably if it works in practice, the theoretical issues are not so big a deal.

taion avatar Oct 12 '17 02:10 taion

Here's a draft of an approach that I'm using. I haven't read the react-router-scroll code but maybe this technique can be used?

To use it just import and call scrollMemory().

import { browserHistory } from 'react-router';

export const scrollMemory = () => {
  const ATTEMPT_RESTORE_MILLISECONDS = 500;
  const REATTEMPTS_TO_SCROLL = 10;

  const memory = {};
  let wasUserGeneratedScroll = true;

  const getScrollTop = () => {
    if (document.body && document.body.scrollTop) {
      return document.body.scrollTop;
    } else if (document.documentElement && document.documentElement.scrollTop) {
      return document.documentElement.scrollTop;
    }
    return 0;
  };

  const stringifyLocation = (location) => {
    return `/${location.pathname.replace(/^\//, '')}${location.search
      ? `?${location.search.replace(/^\?/, '')}`
      : ''}${location.hash ? `#${location.hash.replace(/^#/, '')}` : ''}`;
  };

  window.addEventListener('scroll', () => {
    const location = browserHistory.getCurrentLocation();
    const url = stringifyLocation(location);
    memory[url] = getScrollTop();

    // if they're currently in the middle of attempting
    // to restore a scroll position...
    if (restoreScrollTimer && wasUserGeneratedScroll) {
      // the user has scroll themselves during attempting
      // to restore scroll positions so cancel further attempts
      clearTimeout(restoreScrollTimer);
    }
  });

  let restoreScrollTimer;
  const eventuallyScrollTo = (scrollTop) => {
    if (restoreScrollTimer) {
      clearTimeout(restoreScrollTimer);
    }
    let remainingAttempts = REATTEMPTS_TO_SCROLL;
    const attemptScroll = () => {
      remainingAttempts--;
      wasUserGeneratedScroll = false;
      window.scrollTo(0, scrollTop);
      requestAnimationFrame(() => {
        wasUserGeneratedScroll = true;
      });
      if (remainingAttempts > 0 && getScrollTop() !== scrollTop) {
        restoreScrollTimer = setTimeout(
          attemptScroll,
          ATTEMPT_RESTORE_MILLISECONDS
        );
      }
    };
    attemptScroll();
  };

  browserHistory.listen(location => {
    // fired when the browser changes urls
    const url = stringifyLocation(location);
    if (memory[url]) {
      // there's been a url change and we have a new scroll
      // position to restore
      eventuallyScrollTo(memory[url]);
    }
  });
};

holloway avatar Oct 16 '17 02:10 holloway

I'm sharing my current solution, as I think it's quite nice and I haven't seen it elsewhere in this form (using react-router location state).

It defaults to scrolling to the top on location change (and lets the browser handle "Back" and "Forward" scroll state -- which I've tested to work well in Firefox & Chrome newest versions). It also allows for you to change location without resetting on a case by case basis, using react-router's location state.

// ScrollToTop.js
import React from 'react';
import { withRouter } from 'react-router-dom';

class ScrollToTop extends React.Component {
  componentDidUpdate(prevProps) {
    if (prevProps.location && this.props.location !== prevProps.location &&
      !(this.props.location.state && this.props.location.state.resetScroll === false)) {
      window.scrollTo(0, 0);
    }
  }

  render() {
    return this.props.children;
  }
}

export default withRouter(ScrollToTop);
// client.js
...
<ScrollToTop>
 <App />
</ScrollToTop>
...
// Example usage:
history.push('/about'); // Reset the scroll
history.push('/tab/1', { resetScroll: false }); // Don't reset the scroll

davidfurlong avatar Jun 01 '18 10:06 davidfurlong

For the time being, why not just use @ytase's react-router-scroll-4? I can't guarantee correctness, but it's probably closer to what you want.

taion avatar Jun 01 '18 14:06 taion

@ytase Can you update to use new context instead of legacy?

ntucker avatar Jan 28 '19 02:01 ntucker