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

[v6] animated switch between routes in v6 #7297

Closed
MeiKatz opened this issue Apr 30, 2020 · 24 comments
Closed

[v6] animated switch between routes in v6 #7297

MeiKatz opened this issue Apr 30, 2020 · 24 comments

Comments

@MeiKatz
Copy link
Contributor

MeiKatz commented Apr 30, 2020

Are there any plans on how somebody could realise animated transitions between routes? In v5 we could use the location prop of the <Switch /> component, but in the new <Routes /> component there isn't any replacement for this. Or is there an other way to do it in v6?

@MeiKatz MeiKatz changed the title [v6] animated switch between in routes in v6 [v6] animated switch between routes in v6 Apr 30, 2020
@timdorr
Copy link
Member

timdorr commented Apr 30, 2020

I think two things need to happen:

  • useRoutes needs to accept a provided location.
  • We should switch those positional args to an options object.

The latter is optional, but more scalable as an API.

Then adding a location prop to <Routes> is easy.

@MeiKatz
Copy link
Contributor Author

MeiKatz commented Apr 30, 2020

This sounds good to me. For me it would be fine to only add it to the useRoutes hook. <Routes /> is just a wrapper around it and can be implemented on my own.

@MeiKatz
Copy link
Contributor Author

MeiKatz commented May 1, 2020

Related pull request: #7298

@AdrienLemaire
Copy link

AdrienLemaire commented May 13, 2020

Can someone explain the advantages of adding location in Routes with a usage example?

I personally don't use 3rd-part transition/animation library, and can do animated page transition using useNavigate, useEffects and css.

In the example below, I set a timeout of 500ms in an effect, and a 500ms opacity transition when clicking on a button.

export default function Welcome() {
  const navigate = useNavigate();
  const [path, setPath] = useState<string | null>(null);

  function handleClick(path: string) {
    setPath(path);
  }

  useEffect(() => {
    if (path) setTimeout(() => navigate(path), 500);
  });

  return (
    <div
      className={path ? cn(container,   "transition-opacity", "duration-500", "ease-linear", "opacity-0") : container}
    >
      <Button
        onClick={() => handleClick("/discover/")}
        className={pinkButton}
        disabled={path !== null}
      >
        <span className="m-auto">Discover</span>
      </Button>

I'd like to find a similar approach that would be declarative, though.

@MeiKatz
Copy link
Contributor Author

MeiKatz commented May 13, 2020

@AdrienLemaire Well, within a <Routes /> there can be only one page active at a time. When you want a transition between two pages you want two pages to be active at a time. That means that you need two locations to be used at a time. <Routes /> (like <Switch /> in RRv5) can only handle one location at a time. Therefore you need a way to tell one <Routes /> to use the current location and the other one to use the upcoming location. And here <Routes location /> comes in handy.

@AdrienLemaire
Copy link

AdrienLemaire commented May 13, 2020

@MeiKatz thanks for the clarification, makes sense in order to show parts of both pages at the same time during the animation.

Another thought: Wouldn't the useDeferredValue hook from upcoming concurrent mode also help with that, providing an alternative to the Routes location solution? I have not yet managed to get it working in my case with the experimental branches.

@MeiKatz
Copy link
Contributor Author

MeiKatz commented May 13, 2020

@AdrienLemaire The useDeferredValue hook might help here, but it doesn't challenge the use case of <Routes location />. Therefore the both features might go well together.

Edit: I thought a little bit further: I don't think that useDeferredValue will add any value to this. A AnimatedRoutes component might look like this:

function AnimatedRoutes({
  children,
  classNames,
  timeout,
  ...rest
}) {
  const location = useLocation();

  return (
    <TransitionGroup {...rest}>
      <CSSTransition
        key={location.key}
        timeout={timeout}
        classNames={classNames}
      >
        <Routes location={location}>
          {children}
        </Routes>
      </CSSTransition>
    </TransitionGroup>
  );
}

You see? No need for useDeferredValue.

@stale
Copy link

stale bot commented Jul 12, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
You can add the fresh label to prevent me from taking any action.

@stale stale bot added the stale label Jul 12, 2020
@MeiKatz
Copy link
Contributor Author

MeiKatz commented Jul 12, 2020

This issue is still fresh even so there is a pending pull request.

@woollsta
Copy link

Possible alternative approach with current release without location on Routes, depending how complex your use case...

I have always done these routes transitions using a <TransitionGroup> wrapped around the <Routes> (well, <Switch>) and to pass the location so we can have 2 states of the at once. That was until today when looking for something else I read react-transition-group's recommendations about how to animate between routes. They specifically say:

Note: When using React Transition Group with React Router, make sure to avoid using the Switch component because it only executes the first matching Route. This would make the exit transition impossible to achieve because the exiting route will no longer match the current URL and the children function won't execute.

In their example for v5, they just map over the routes (no parent Switch) and use the function child variant of Route to decide if the child route component is rendered or not based on the match. See below:

    <Router>
          ...
          {routes.map(({ path, Component }) => (
            <Route key={path} exact path={path}>
              {({ match }) => (
                <CSSTransition
                  in={match != null}
                  timeout={300}
                  classNames="page"
                  unmountOnExit
                >
                  <div className="page">
                    <Component />
                  </div>
                </CSSTransition>
              )}
            </Route>
          ))}
          ...
    </Router>

From what I can tell, v6's <Route>s dont support this functional children variant. They do however have hooks we can use to do the same. So I did the following:

  1. I removed <Routes /> (possibly now going to break other code since it doesnt create a new context, anyone care to comment?).
  2. I made a new type of <Route /> component that looks something like this:
const MyRoute = ({ path, element }) => {
  const resolvedPath = useResolvedPath(path);
  const match = useMatch(resolvedPath.pathname);
  return (
    <CSSTransition
        in={match != null}
        timeout={300}
        classNames="page"
        unmountOnExit
      >
         <div>
           {element}
         </div>
    </CSSTransition>
  );
}
  1. Refactored the routes so they look like this:
    <BrowserRouter history={history}>
          ...
          {routes.map(({ path, element }) => (
            <MyRoute key={path} path={path} element={element} />
          ))}
          ...
    </BrowserRouter>

The reason this works is because all <MyRoutes /> get rendered, and the decision to render the element is deferred to the <Transition>'s in={match != null} prop. One nice property of this approach is being able to keep a Route rendered but hidden by changing the properties on the Transition to unmountOnExit={false} mountOnEnter={true}.

I have a feeling that <Routes /> does more than this simple case, so removing it may break nested routes. Has anyone else got any other feelings on this approach?

Right now, I'm using this in one of my nested route outlets for a specialised case. Seems to be doing what I expect.

@MeiKatz
Copy link
Contributor Author

MeiKatz commented Oct 13, 2020

@woollsta You're right, but the approach I realised in my referenced PR makes it a lot easier. Currently the problem is, that @mjackson and @ryanflorence are busy doing other stuff and therefore RRv6 makes no progress at all. Hopefully this will change in the near future. Would be a pity if not.

@woollsta
Copy link

This is likely a stupid question, but is there a way to reference the patched branch in my project so I can try it out prior to it's eventual merge? I gave it a go, but I think bc of the way the monorep is set up I'm not able to reference the packages inside. E.g. tried yarn add https://github.com/smvv/react-router.git#add-location-prop to no avail.

@MeiKatz
Copy link
Contributor Author

MeiKatz commented Oct 13, 2020

You can clone my repository and run the install script..

@tranvansang
Copy link

tranvansang commented Oct 28, 2020

in my project, I didn't use Switch either to achieve animation. Instead, I found that at least in v5, Route exposes a computedMatch props, which enables me to use matchPath to get the matched route and its matched params (something like <Route computedMatch={matched}><matchedRoute.component/></Route>.

I also use react-transition-group, which remembers the props of the last (the going to disappear) route rendering. So that it does not give the incorrect matched params while the animation happens.

I haven't tried with v6 yet. But hope that there will be another workaround if the computedMatch props is removed.

@jymbob
Copy link

jymbob commented Dec 10, 2020

I've just come across this attempting to pair react-spring with React Router v6.

Example with v5 (linked from react-spring): https://codesandbox.io/s/jp1wr1867w - old component leaves, new component enters

Example with v6: https://codesandbox.io/s/react-router-spring-b5941 - old component is immediately replaced, then animates out and back in

@MeiKatz could you explain what changes, if any, I'd need to get this working with your PR?

@MeiKatz
Copy link
Contributor Author

MeiKatz commented Dec 10, 2020

@jymbob in RRv6 there is no <Switch /> component. The replacement is the <Routes /> component that does nearly the same but with one essential difference: it has no location prop. The only location that is available comes from the routing context. And therefore you cannot implement route transitions at all.

@jymbob
Copy link

jymbob commented Dec 10, 2020

@MeiKatz I understand that's true as of now. Would your PR bring back this ability, or have I misunderstood?

@MeiKatz
Copy link
Contributor Author

MeiKatz commented Dec 10, 2020

@jymbob Definitely! <Routes /> uses the hook useRoutes() internally. If we add the location prop to useRoutes() and <Routes /> we could change the location that all routes below are matched against.

The only reason why this does not work, is that @ryanflorence and @mjackson are currently working on their main project and have no time for RR. Actually, that is kind of a really bad situation right now.

@mkaizad
Copy link

mkaizad commented Jan 9, 2021

I'm running into the same issue, thank you for addressing it @MeiKatz. I hope your fix is merged soon.

@MeiKatz
Copy link
Contributor Author

MeiKatz commented Jan 9, 2021

@Meenaaaa I hope so, I hope so. If there is no fix until end of 2021 we should think about a fork of RR.

@AdrienLemaire
Copy link

Looking forward to seeing you champion the fork @MeiKatz <3

@sahil87
Copy link

sahil87 commented May 10, 2021

@MeiKatz if I may ask, it's May of 2021, what are you using for animated route switching now?
Is the easiest way to do this via patch-package on #7298 ? (But that PR once again has merge conflicts)

+1 for a new fork btw.

@MeiKatz
Copy link
Contributor Author

MeiKatz commented May 10, 2021

@MeiKatz if I may ask, it's May of 2021, what are you using for animated route switching now?
Is the easiest way to do this via patch-package on #7298 ? (But that PR once again has merge conflicts)

+1 for a new fork btw.

I have not switched to V6 yet. And this issue is one of the reasons why.

@chaance
Copy link
Collaborator

chaance commented Jul 28, 2021

Duplicate of #7117, we'll share updates there.

@chaance chaance closed this as completed Jul 28, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants