Diogo Izele

2026-09-19

JS Stack vs. Native Stack: What Actually Changes?

React NativeNavigationReact NavigationNative

The vast majority of apps, if not all of them, have some kind of navigation mechanism. At the end of the day, it’s nothing more than a way to manage the transition between screens. In general, the usual approach is not to reinvent the wheel and instead use a library that already exists and is widely adopted by the industry, such as React Navigation.

As a developer, you’ll probably agree that architectural decisions rarely affect only the person making them. They’re usually made at the beginning of a project, but their consequences can stick around for years, especially for the people who have to maintain, evolve, or eventually change whatever was decided.

Most of the time, you join a team and become hostage to decisions that were made before you got there. And changing an architectural decision while keeping the entire system working can become a utopian future.

That’s why, even without knowing every implementation detail, it’s important to understand the options a library like React Navigation provides and the trade-offs between them. In this case, we’re going to take a closer look at its two Stack implementations: JavaScript Stack and Native Stack.

React Navigation provides two implementations of a navigation stack.

In short, a navigation stack is a data structure that organizes the app’s screens into a sequence, allowing the user to navigate forward and backward between them. Think of a browser when you click a link and then use the back button to return to the previous page. It works the same way: when you move forward, the current screen is pushed onto the stack, and when you go back, a screen is removed, revealing the previous one.

JavaScript Stack

The first implementation is known as the JS Stack. Although this isn’t its official name, it’s commonly used to distinguish React Navigation’s default implementation from the Native Stack.

In the JavaScript Stack, the main navigation logic is controlled by React Navigation. It maintains the stack state and determines how screens enter, leave, and transition between each other.

That doesn’t mean everything runs in JavaScript. The implementation uses native libraries such as react-native-gesture-handler, react-native-reanimated, and react-native-screens, which take on responsibilities related to rendering, gestures, and animation execution.

The difference is that the logic coordinating the transition is still implemented by React Navigation on the JavaScript side.

Creating a JavaScript Stack

To create a navigation stack, we use createStackNavigator:


import { createStackNavigator } from '@react-navigation/stack';

const Stack = createStackNavigator();

The fact that this is React Navigation’s default implementation has a historical reason. It was created first, at a time when React Native didn’t yet offer such comprehensive support for native navigation.

As a result, building navigation in JavaScript also provided greater control over its behavior and customization options.

Under the hood

When we push, pop, or navigate to another route, React Navigation updates the stack state in JavaScript. This is when the library determines which routes are active and how the stack should be reorganized.

Since we’re talking about React, this causes the component representing the navigation stack to re-render, which in turn renders the components corresponding to the screens.

After that, the Stack implementation needs to drive the transition between screens. It determines, for example, how a screen enters or leaves, how its position changes during the animation, and how it responds to a swipe gesture.

This is the main characteristic of the JS Stack: the transition logic is implemented by React Navigation.

The libraries used by the implementation take on different responsibilities:

  • react-native-gesture-handler recognizes and tracks gestures.

  • react-native-reanimated allows animations to run more efficiently, including outside the JavaScript thread.

  • react-native-screens allows native screen components to be used and improves the management of screens that are out of focus.

Using these libraries doesn’t change who controls the navigation. The screen can be native, the gesture can be processed with native code, and the animation can run independently of the JavaScript thread. Even so, the logic coordinating the transition is still orchestrated by JS.

That’s precisely what makes the JS Stack so flexible. If the library needs to decide that a screen should slide in from the right, scale, disappear using a particular interpolation, or respond to a user gesture in a specific way, that logic can be built and changed directly in the JavaScript implementation.

But here’s a question worth asking: how often are we actually creating navigation that differs from the standard?

In most apps, we’re probably doing the exact opposite: following patterns that users already know and that the market has already validated.

Besides, sometimes we aren’t even the ones choosing the implementation. You simply follow whatever pattern already exists in the project.

That flexibility, of course, comes at a cost. Since part of the navigation logic is implemented in JavaScript, more complex navigation can involve additional work in this abstraction layer and in the communication with the native side.

In smaller applications, this performance difference might not even be noticeable. In more complex scenarios, however, it can become relevant.

The flow can be represented like this:

Navigation state → Stack logic → gesture/animation → native components → screen

And this is precisely what changes when we move to the Native Stack.

Native Stack

And here comes the first little trap: the Native Stack also uses react-native-screens to render and manage screens.

So, the difference between the two implementations isn’t that one renders in JavaScript while the other renders natively. What changes is who controls the navigation and its transitions.

To create a Native Stack, we use createNativeStackNavigator:


import { createNativeStackNavigator } from '@react-navigation/native-stack';

const NativeStack = createNativeStackNavigator();

From there, the architecture works differently.

This implementation still receives the routes and options defined in JavaScript. The difference is that, in the Native Stack, it doesn’t implement the navigation and its transitions itself.

That responsibility is passed to react-native-screens, which acts as the bridge between React Navigation’s API and each platform’s native primitives. @react-navigation/native-stack itself relies on react-native-screens to provide this native implementation.

It’s important to understand that React Navigation isn’t directly calling a UINavigationController or manipulating a Fragment. react-native-screens is responsible for this integration. On iOS, it uses UINavigationController, while on Android, it uses components based on Fragment.

This is where it’s worth making a distinction that can easily go unnoticed when we look only at the library names. react-native-screens appears in both approaches, but it isn’t doing exactly the same job in each one.

In the JavaScript Stack, it can be used to replace regular React Native Views with native screen components. This allows the operating system to participate in managing screens that are out of focus, reducing the amount of memory being used.

In the Native Stack, however, react-native-screens stops being merely infrastructure used by the navigation and becomes the fundamental native mechanism behind the Stack, ScreenStack, which receives ScreenStackItems and places them into a native navigation structure.

ScreenStack maintains the native stack structure, and changes to that structure are translated into each platform’s navigation APIs. From there, the platform itself can perform the transition and handle gestures using its native primitives.

In practice, we can think of the flow like this:

Navigation state

React Navigation

Native Stack

react-native-screens

native implementation

UINavigationController / Fragment

native transition and management

This also explains why saying that "Native Stack uses UINavigationController and Fragment" can be imprecise and hide an important part of the architecture.

A more precise description would be: React Navigation uses the mechanisms provided by react-native-screens to implement navigation and transitions, which are executed on the native side.

This helps explain why Native Stack is less flexible. When a navigation option depends on behavior managed by the native side, react-native-screens can expose that capability to React Navigation.

When we want something that falls outside the model supported by the native side, we’re no longer just customizing a JavaScript implementation. We’re running into the limits of the native implementation underneath it.

What changes during navigation?

In the JS Stack, when a screen enters or leaves the stack, React Navigation needs to control the transition through its own implementation.

In the Native Stack, React Navigation tells the native side what needs to happen. From there, the platform components take over navigation.

UINavigationController already knows how to manage a stack of UIViewControllers, perform the transition, and handle the header and native back gesture. The Fragment-based integration plays an equivalent role within the architecture used by react-native-screens.

JavaScript is still important because it remains part of the integration, maintains the navigation configuration, and coordinates communication with the native side. What changes is where the main part of the navigation actually happens.

The Native Stack trade-off

The JS Stack offers more freedom because React Navigation controls the implementation of the transitions. This makes it possible to deeply alter their behavior, as long as the implementation provides the necessary mechanisms.

With the Native Stack, the behavior stays closer to what iOS and Android already provide. On the other hand, the available customization options depend on the platform APIs and on what react-native-screens exposes.

And this limitation doesn’t necessarily show up only when we want to change an animation. In larger applications, choosing Native Stack can end up affecting the way navigation itself is structured. I’ve run into this kind of situation in practice, and it’s a good example of how the trade-off can involve architectural decisions, not just the visual behavior of navigation.

This difference matters when choosing an implementation.

It’s not simply a matter of choosing between JavaScript and native code. It’s about choosing where the navigation logic will run, how much control the application will have over it, and which architectural decisions that choice may end up constraining.

The Difference That Actually Matters

After looking at both implementations, it becomes easier to understand where the names JS Stack and Native Stack come from, and the confusion they can cause.

Both use JavaScript. Both can use react-native-screens. Both can end up displaying native components.

The difference isn’t simply where the screen is rendered.

It’s who controls the navigation and its transitions.

In the JavaScript Stack, React Navigation implements the stack logic in JavaScript and uses native capabilities to execute parts of that work.

In the Native Stack, React Navigation mainly acts as a configuration and integration layer, while the platform’s native components take responsibility for navigation and transitions.

Aspect JS Stack Native Stack
Navigation implementation React Navigation, in JavaScript Native platform components
Screen management react-native-screens react-native-screens
Transitions Implemented by React Navigation Use the platform’s native primitives
Gestures Stack implementation integrated with native resources Platform gestures, according to the implementation used
iOS React Navigation’s own logic UINavigationController
Android React Navigation’s own logic Fragment-based integration
Customization Greater control over transition implementation Depends on the platform and library capabilities
Performance May show differences in more complex scenarios Very close to native behavior

What does this mean in practice?

You’ll rarely need to think about this on the day you create your screens. The difference starts to show when navigation becomes more complex and some decisions that seemed small begin to come back for their payment.

I’ve been through this in practice, and it was precisely while working on an existing navigation implementation that it became clearer to me that choosing a Stack isn’t just about choosing how screens transition. That’s why, before choosing between JS Stack and Native Stack, it’s worth understanding what each one is doing under the hood.

The better you understand the tool, the less likely it is to become a black box when the application starts getting complicated.