expo / router

[ARCHIVE]: Expo Router has moved to expo/expo -- The File-based router for universal React Native apps

Home Page:https://docs.expo.dev/routing/introduction/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Protected pages accessible when they should not be?

janusqa opened this issue · comments

Which package manager are you using? (Yarn is recommended)

npm

Summary

Before expo-router there was a clear delineation between which stack was loaded based on if a user was "logged" in or not. They were two separate stacks with no relation between them. the auth stack and the app stack.
With expo-router I seem to be running into an issue where these two conceptual stacks seem to intertwine. See the following.

useProtectedRoute.ts

import { type User } from "../services/UserService";
import useNavigationReady from "./useNavigationReady";

const useProtectedRoute = (user: User | undefined) => {
	const segments = useSegments();

	const isNavigationReady = useNavigationReady();

	useEffect(
		function () {
			const isAuthGroup = segments[0] === "(auth)";
			if (isNavigationReady) {
				if (!user?.email && !isAuthGroup) {
					router.replace("/(auth)");
				} else if (user?.email && isAuthGroup) {
					router.replace("/(app)/(tab2)");
				}
			}
		},
		[segments, user?.email, isNavigationReady]
	);
};

export default useProtectedRoute;
/app
   /(app)
        /(tab1)
              /index
        /(tab2)
        /(tab3)
        /_layout.tsx
   /(auth)
         /index
         /login
         /register
_layout.tsx

I have the above layout. I am experiencing a problem, where although /(auth) ultimately loads if there is no user. However
some weird magic seems to be happening behind the scenes where it seems like /(app)/(tab1)/index is being accessed first somehow even though its not displayed.
I know because I have some code there which runs when the app loads and no user is logged in. This should not be, given its a protected route.

The expectation was that /(auth) should load without any of the screens in protected route coming into play

Anyone else experiencing this?

Minimal reproducible example

https://github.com/janusqa/rn-bare

When the app is loaded the welcome screen should be displayed and in the console you should see
SplashScreen.preventAutoHideAsync() succeeded: true

Instead I see

SplashScreen.preventAutoHideAsync() succeeded: true
accessing protected screen tab 1 / screen 1...

Since tab 1 / screen 1. is a protected screen I should not see it.

One other issue I see while doing this repro-repo is clicking back and forth thru tabs 1 and 2
When "logged in"
If i click on tab 2 I see "accessing protected screen tab 2 / screen 1..."
if i click back on tab one i still see "accessing protected screen tab 2 / screen 1...", does not look right
If i click back and forth after that I see no more console logs to indicate code was run when rendering the screen anymore.

However
some weird magic seems to be happening behind the scenes

app/(app)/(tab1)/index conforms to / so naturally it will be loaded first if the app is launched from the home screen or a deep link that conforms to /. There's no built-in route protection or middleware, so your current approach will have to consider that redirects are taking place after routes have already rendered. If you need the logic in the index route to not be evaluated then you may want to consider restructuring your app such that / conforms to an intermediary route that loads content and redirects to a different route like /home after.

Hi @janusqa - what's happening is the path / matches two routes, app/(app)/(tab1)/index and app/(auth)/index, and Expo Router needs to disambiguate between the two when there's no other context.

Choosing a route for a path

The policy Expo Router currently uses is to prioritize (app) over (auth) because both are groups and (app) is shorter. It makes sense to document how route precedence is chosen and perhaps choose a different policy like lexicographical ordering but the current logic is in the linked code snippet. This precedence policy is the only "magic" here and the rest of the behavior is common JavaScript and React.

Shared/parallel routes: As an aside, the reason for multiple routes to satisfy the same path is for a feature called parallel routes, which is when you might be on an screen in the (auth) group and navigate to / in that context. This matters more for navigating within a tab associated with a group.

Back to the code in the original post, opening your app to / will always load app/(app)/(tab1)/index. The useProtectedRoute hook in your application code checks whether the user is authenticated, but it does not affect the router's logic for routing / to app/(app)/(tab1)/index. After all, the hook runs after the router has chosen the component at app/(app)/(tab1)/index and the component is already being rendered.

Routes are not protected

Perhaps a more accurate name for the hook would be useAuthenticationCheck. The hook does not prevent a route from being chosen by the router. The hook just checks your app's authentication state for the user and decides whether to redirect the user to a different route or to keep rendering the children of the current component.

"Components that perform an authentication check when rendered" is a more accurate mental model than "protected pages". @EvanBacon's suggestion above is one way to organize your components.

Hook Effects run asynchronously

Additionally, React runs Hook Effects asynchronously. So even if your Effect redirects the user, the redirect Effect won't run until the component containing that hook has already been rendered once:

function Example() {
  useEffect(() => {
    router.replace("/(auth)");
  }, []);

  // This View will *always* be rendered for a moment before the Effect runs
  return <View />;
}

Additionally, server-side rendering doesn't run Effects and renders the children, another case to handle.

Takeaways

In the short-term, two achievable improvements would be to make several of these behaviors clearer in the docs: explain the precedence rules for routes that satisfy the same path and explain that the Hook schedules an authentication check in an Effect, which runs asynchronously.

Thank you!
Will try these suggestions.