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

[Feature]: Use case for public UNSAFE_LocationContext? #8867

Closed
orderedspinach opened this issue May 13, 2022 · 3 comments
Closed

[Feature]: Use case for public UNSAFE_LocationContext? #8867

orderedspinach opened this issue May 13, 2022 · 3 comments
Labels

Comments

@orderedspinach
Copy link

orderedspinach commented May 13, 2022

What is the new or updated feature that you are suggesting?

Making UNSAFE_LocationContext available publicly.

Background

I am currently using React Router to manage page transitions for my web app, and for animations on route transition I am using React Transition Group. I know that as of RRv6 it's become somewhat difficult to implement CSS Transitions the way that it could be done in v5. While I think I may have come up with a solution, the caveat here is that it makes use of some of React Router's internal apis, which I would like to discuss here.

Code

/**
 * Component wrapper that provides Animation on route transition.
 */
function AnimatedTransition(
  props: React.PropsWithChildren<{ routeObjects?: RouteObject[]; animationConfig?: AnimationConfig }>
): JSX.Element {
  /**
   * React Router has two ways to render routes:
   *  1. via the <Routes/> component/useRoutes hook. Both root and child routes can be rendered this way.
   *  2. via an <Outlet/> component. These components render content associated with children Route elements only.
   * We want to support transitions for both these cases in tandem, which we can do by separating routing logic for root
   * routes and children routes:
   *  - When 'routeObjects' are provided, generate the CSSTransition key from the first match of these routeObjects.
   *    This will handle transitions for top-level routes only.
   *  - Otherwise, generate the CSSTransition key based on the first subPath of the remainingPathname. This allows
   *    us to handle immediate child routes, but also not cause transitions when transitioning to further nested
   *    child routes.
   */
  const location = useLocation();
  const navigationType = useNavigationType();
  const { pathname: parentPathnameBase } = useResolvedPath('');
  const pathname = location.pathname || '/';
  const remainingPathname =
    parentPathnameBase !== '/' && pathname.startsWith(parentPathnameBase)
      ? pathname.slice(parentPathnameBase.length) || '/'
      : pathname;
  const matches = matchRoutes(props.routeObjects || [], {
    pathname: remainingPathname,
  });
  const key = props.routeObjects ? matches && matches[0].pathnameBase : remainingPathname.split('/')[1];

  return (
    <TransitionGroup
      appear={!!props.routeObjects}
      component={null}
      childFactory={(child) =>
        props.animationConfig
          ? React.cloneElement(child, props.animationConfig)
          : child
      }
    >
      <CSSTransition timeout={0} key={key}>
        <UNSAFE_LocationContext.Provider value={{ location, navigationType }}>
          {props.children}
        </UNSAFE_LocationContext.Provider>
      </CSSTransition>
    </TransitionGroup>
  );
}

/**
 * Renders an <Routes/> component that supports Animations.
 */
export function AnimatedRoutes(props: { routeObjects: RouteObject[]; animationConfig?: AnimationConfig }): JSX.Element {
  const routes = useRoutes(props.routeObjects);
  return (
    <AnimatedTransition animationConfig={props.animationConfig} routeObjects={props.routeObjects}>
      {routes}
    </AnimatedTransition>
  );
}

/**
 * Renders an <Outlet/> component that supports Animations.
 */
export function AnimatedOutlet(props: OutletProps & { animationConfig?: AnimationConfig }): JSX.Element {
  // <Outlet> components get their context based a RouteContext that updates on location change. Passing it into
  // <AnimatedTransition/> directly will result in a phenomenon where the onExit content will re-render to be the
  // onEnter content, giving the impression of transitioning from one page to the exact same page.
  // We can prevent this by getting the outlet content outside of AnimatedTransition and passing this object in instead.
  const outlet = useOutlet(props.context);
  return <AnimatedTransition animationConfig={props.animationConfig}>{outlet}</AnimatedTransition>;
}

Manually setting LocationContext

As you can see, I use UNSAFE_LocationContext within the AnimatedTransition component. The reason is that when a component within a TransitionGroup has a key update, the new-keyed content (onEnter) is rendered alongside the old-keyed content (onExit). However, if the content has a descendant component that relies on the useLocation or useSearchParams hooks, the component will reference the current URL, even within the onExit component.

For example, in the below case:

function LocationPage(props: { to: string}) {
  const location = useLocation();
  const navigate = useNavigate();
  const onClick = () => navigate(props.to);
  return (
    <div>Welcome to {location.pathname}
      <button onClick={onClick}>Next</button>
    </div>
  )
}

export function Example() {
  const routeObjects = [
    { path: 'test_A', element: <LocationPage to="/test_B"/>},
    { path: 'test_B', element: <LocationPage to="/test_A"/>}
  ]
  return <AnimatedRoutes routeObjects={routeObjects} animationConfig={{ timeout: 500, classNames: ''}}/>
}

Clicking the Next button while on the test_A route will cause the current content to immediately change to "Welcome to test_B" , even before the transition has finished.

I've found that they way around this is to set the location context of the component within TransitionGroup to the location from the scope in which the component is declared, which seems only possible with UNSAFE_LocationContext.

Rendering with Outlets

Using Outlets with AnimatedTransition seemed straightforward enough, but I noticed that on transition, the onExit component would always have its content change to reflect outlet content at the current location, giving the impression of transitioning to duplicate screen. This occurs regardless of whether or not UNSAFE_LocationContext is used.
I was able to get animations working properly by using the useOutlet hook, but I want to ask: is there something I am missing here? Is there perhaps a better way to guarantee that onExit outlet context reflects the route the outlet is reference at the time the route was entered?

Why should this feature be included?

It seems to me that being able to set LocationContext freely in this manner is actually really useful, so I was curious if it would be possible to make this okay for public/normal use?

@timdorr
Copy link
Member

timdorr commented May 14, 2022

This is the same issue as #8470

@timdorr timdorr closed this as completed May 14, 2022
@orderedspinach
Copy link
Author

@timdorr I can see similarities between this and #8470, but it seems like for that issue, it was solved by using UNSAFE_LocationContext, whereas this topic is more about providing the LocationContext as a safe to use API (i.e. promoting it from an purely internal API).
I only provide the Animation Transition as an example to show the merits of using UNSAFE_LocationContext, but I do think that there are potentially other use-cases other than Animations.

@GreenAsJade
Copy link

... for example: https://stackoverflow.com/a/72683269/554807 ?

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

No branches or pull requests

3 participants