React Navigation & useMemo: When navigate Refuses to Run

Introduction

If you’ve worked with React Native and React Navigation, you may have faced a frustrating situation:
navigation.navigate(...) works once, but the second time it does nothing.

At first glance, this feels like a bug in React Navigation, but it’s actually a side effect of how it checks for route changes, combined with how we sometimes “optimize” with useMemo.

In this post, let’s break down why this happens, walk through real examples, and cover practical solutions.

How React Native Navigation Works Behind the Scenes

React Native navigation libraries, such as React Navigation or React Native Navigation (by Wix), don’t actually switch between physical “screens” like a native Android or iOS Activity stack. Instead, they simulate a native navigation flow by managing a JavaScript-based navigation state that controls which component is rendered at any given moment.

When you navigate between screens, the library updates this navigation state (usually stored in context or a navigation container) and triggers a re-render. Each “screen” is just a React component, but the navigation library wraps them with platform-specific native views (like UIViewController on iOS or Fragment on Android) to ensure transitions, headers, and gestures feel native.

In short, React Native navigation is a clever combination of JavaScript state management and native container integration, giving you smooth transitions and deep navigation stacks, all while keeping your app in a single React render tree.

Using React Navigation in Typical Scenarios

In most React Native projects, navigation feels straightforward, you call a function, and the app moves to the desired screen.
For simple one-level navigation, something like this works perfectly:

navigation.navigate('Profile', { userId: 42 });

But in more complex apps, especially those with nested navigators (for example, a “root” tab or stack containing child screens), we often navigate to a root route and pass along params that describe the inner screen we want to open.

Let’s say we have a root called Main which contains multiple stacks:

<Stack.Navigator>
  <Stack.Screen name="Main" component={MainTabs} />
  <Stack.Screen name="Settings" component={SettingsScreen} />
</Stack.Navigator>

Inside MainTabs, you have something like:

<Tab.Navigator>
  <Tab.Screen name="Home" component={HomeScreen} />
  <Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>

Now, from anywhere in the app, you can navigate to the Main root and specify which tab or nested screen to show:

navigation.navigate('Main', {
  screen: 'Profile',
  params: { userId: 42 },
});

Why does navigate sometimes not work?

navigation.navigate(name, params) decides whether to run based on two checks:

  1. Screen name (route)
    If you’re already on the same screen, it won’t navigate again.
  2. Params object
    If the params you pass is identical (both by value and reference), React Navigation assumes nothing has changed, so it doesn’t navigate.

This is where useMemo or reusing objects can cause trouble.

Case Study 1: Build params inline (works fine)

navigation.navigate(
  RootStackRoutes.UserStack,
  buildUserDetailParams(userId)
);

Every time you call buildUserDetailParams, you create a new object.
→ React Navigation sees a new params object → navigation works.

Case Study 2: Memoized params (fails on second navigate)

const userParams = useMemo(
  () => buildUserDetailParams(userId),
  [userId]
);

navigation.navigate(RootStackRoutes.UserStack, userParams);

Here, userParams is memoized.

  • First time: it works fine.
  • Second time: since userId hasn’t changed, userParams keeps the same reference and value.
    → React Navigation thinks it’s the same request → it skips navigation.

Case Study 3: Reusing the same object

const params = { screen: UserStackRoutes.UserDetail, params: { userId: 123 } };

navigation.navigate(RootStackRoutes.UserStack, params);
navigation.navigate(RootStackRoutes.UserStack, params); // ignored

Solutions

Solution 1: Always build params inline

navigation.navigate(
  RootStackRoutes.UserStack,
  buildUserDetailParams(userId)
);

Solution 2: Clone the memoized object

const userParams = useMemo(
  () => buildUserDetailParams(userId),
  [userId]
);

const goToUserDetail = () => {
  navigation.navigate(RootStackRoutes.UserStack, { ...userParams });
};

By spreading {...userParams}, you create a new reference while keeping the same structure.

Solution 3: Use push instead of navigate

navigation.push(RootStackRoutes.UserStack, userParams);

Unlike navigate, push always creates a new entry in the navigation stack, even if the params are identical.

Solution 4: Extract into a custom hook

function useUserDetailNavigation(navigation) {
  return (userId: number) => {
    const params = buildUserDetailParams(userId);
    navigation.navigate(RootStackRoutes.UserStack, params);
  };
}

// usage
const goToUserDetail = useUserDetailNavigation(navigation);
goToUserDetail(123);

This avoids clutter while ensuring params are always built fresh.

Conclusion

The issue of “navigate not working the second time” usually comes from reusing the same params object, either via useMemo or a shared variable. This isn’t a bug—it’s React Navigation’s optimization at work.

The golden rule: always ensure params are a fresh object if you want navigation to trigger.

For reusable logic, wrap it in helper functions or custom hooks, but create a new object when calling navigate.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top