TLDR:
Perfect React Native tech stack recommended by Pagepro:
- Expo
- TypeScript
- Expo Router
- Jest
- React Query
- MMKV
- Native Wind
- Reanimated
- Sentry
The article was first published on our Pagepro blog and was written by our React Native expert, Michal Moroz.
INTRODUCTION
If you’re here, you’re likely seeking the best tech stack for mobile app development in 2024, particularly if you’re **considering cross-platform app development with React Native. **The tech stack choice can significantly impact the app’s performance, development speed, and ultimately, its success.
Of course, the ideal tech stack for mobile development often varies depending on the specific requirements of the app and the preferences of the customer. However, in this discussion, we’ll focus on the tech stack most commonly used and recommended by experts in React Native app development.
React Native, known for its efficiency in building cross-platform applications, continues to evolve. This evolution enhances its capabilities, making it an even more attractive option for mobile app development. UberEats and Bloomberg build their apps using RN and are among the high-profile examples that highlight its practicality and effectiveness across different industries.
We aim to guide you through this mobile app development technology world, providing insights into the best solutions for a React Native tech stack in 2024.
We’ll draw from our experience gained across various industries and cooperation with such big companies like Voucher Codes, whose site last year received over 73 million visits and saved shoppers over £65 million and Admiral, the leader of the insurance industry in the UK to help you make a decision that aligns with your project’s goals.
REACT NATIVE IN 2024: AN OVERVIEW
Lately, React Native enhanced its cross-platform mobile app development capabilities with version 0.73. Key upgrades included a new architecture for improved app performance, TypeScript integration for more efficient coding, and Flexbox Gap for simpler layouts. Enhanced DevTools and more intuitive props and styles streamlined development, while improved symlink support and package compatibility were introduced. These advancements reinforce React Native’s position as a leading framework for efficient, scalable mobile app development in 2024.
Why are CTOs and Tech Leaders increasingly choosing React Native as their core technology for cross-platform development? This preference stems from a variety of reasons that balance both business and technical considerations:
Wix, a globally known website building platform, who decided to use React Native for building their mobile app while already owning a web app built with React, admits that it’s the code sharing between apps they value:
In fact, ~95% of our business logic codebase is shared between iOS app and Android. Furthermore, few of our teams chose to share their business logic code with the Web platform as well, meaning, a feature is implemented by a single team for 3 different platforms without sacrificing the user experience.
Source: Medium – Wix Engineering
Walmart, as of January 2023, the largest retailer in the world chose React Native because of its productivity, among others:
Here are the benefits we observed with React Native:
95% of the codebase is shared between iOS and Android
No knowledge sharing required, as each feature is implemented by a single team
Developer experience is awesome. No need to restart packager to see simple changes
React Native is written in JavaScript. We can leverage programming skills/resources across the organization
Source: Walmart Global Tech
And our customer, Admiral Pioneer, the leader of the insurance industry in the UK, choose React Native for Veygo, their new app for car insurance, because of the fast time to market: read our case study.
KEY COMPONENTS OF REACT NATIVE TECH STACK
The underlying technology stack in React Native applications in 2024 has not changed much compared to the previous year. Some of the technology remains irreplaceable and there is no competition. Below I will list the most basic stack.
Please note that depending on the case, you should use libraries that are not considered basic. The key components of a typical React Native tech stack include:
Platform for building – Expo
Expo is a framework and platform for building React Native applications. It aims to provide a simplified development experience. Simplifying the setup and build process, it facilitates the development of JavaScript and React Native mobile apps by removing common configuration challenges.
The main advantages of the Expo are:
- simplified build process
- EAS
- quick start development
You will find more about the expo in the chapter below: Trends and Innovations: Focusing on Expo.
Language – Typescript
TypeScript is a statically typed superset of JavaScript that adds static types to the language. TypeScript offers all of JavaScript’s features, and an additional layer on top of it: TypeScript’s type system.
Key features of Typescript:
- Static type checking
- Support newer ECMAScript standards
- Interfaces & types
- Generics
The main reasons to use Typescript in new projects are:
- Strong Typing: TypeScript introduces static typing to JavaScript. This helps detect typing errors during the development stage, which can minimize errors.
- Code Readability: Typescript increases code readability. It is easier for other developers to understand the structure and intent of the code.
- Security Improvement: Preventing runtime typing errors safeguards your code against specific error types, ensuring greater reliability in applications.
- Ecosystem and Community: The popularity of TypeScript is on the rise, and numerous open-source projects provide TypeScript type definitions, simplifying integration with a variety of libraries and tools.
TypeScript has become our standard choice for projects due to its robust static typing system, which enhances code quality and reduces runtime errors, resulting in more reliable and maintainable applications. - Jakub Dakowicz, CTO at Pagepro
Below is an example of a simple addNumbers function in JavaScript and TypeScript, along with a brief overview of the differences between them:
JAVASCRIPT
// Sample function in JavaScript
const addNumbers = (a, b) => a + b;
// Function call
const sum = addNumbers(2, 4); // Result: 6
TYPESCIRPT
// Sample function in TypeScript
const addNumbers = (a: number, b: number): number => a + b;
// Function call
const sum: number = addNumbers(2, 4); // Result: 6
As you can see, TypeScript allows you to specify parameter types and the return type of functions. The above example uses numbers as the types for parameters a and b, and for the returned result. This makes the function safer to use because we know what types it takes and what type it returns.
State management – Zustand
Zustand is a React state management library known** for its lightweight, minimalistic design.** Offering simplicity and ease of use, it delivers a versatile and effective solution for handling the state of your React components.
Below, you’ll find essential characteristics of Zustand and compelling reasons to contemplate its adoption:
- Simplicity: Zustand aims to keep things simple by offering a minimal API. It is easy to understand and has a low learning curve, making it accessible to developers of all skill levels.
- Bundle size: Zustand has a small footprint, which contributes to a smaller package size for your React app. This is beneficial for performance, especially in scenarios where minimizing the JavaScript package size is crucial.
- Hooks-based API: Zustand uses React Hooks, which makes it a natural choice for React applications. You can use it with function components and use React hooks like useEffect and useContext to manage the application state.
- No Global State Management: Zustand does not impose a global approach to condition management. You can create isolated stores for specific parts of your application, which makes it flexible and allows you to manage the state in a way that fits your project’s architecture.
- Performance: Zustand is designed with performance in mind. It handles state updates efficiently and ensures that only components that are affected by a specific state change are re-rendered.
- No Boilerplate: Requires minimal boilerplate code to configure and use Zustand. You can create stores and access states and actions using concise syntax, which reduces the amount of code you have to write.
- Reactivity: Zustand uses a reactive model to update the state. Components that subscribe to the store are automatically re-rendered when the state changes, simplifying the process of synchronizing the UI with the application state. In the example below, we establish a store containing a count state variable along with two functions: increment and decrement, dedicated to altering that state. We employ the create function from Zustand to formulate the store, wherein a function is provided, taking a set function as its parameter. The set function serves the purpose of state modification.
Example
import React from 'react';
import { Button, Text, View } from 'react-native';
import { SetState, create } from 'zustand';
type Store = {
count: number;
increment: () => void;
decrement: () => void;
};
const useStore = create<Store>((set: SetState<Store>) => ({
count: 0,
increment: () => set((state: Store) => ({ count: state.count + 1 })),
decrement: () => set((state: Store) => ({ count: state.count - 1 })),
}));
export const CounterFunction = () => {
const { count, increment, decrement } = useStore();
return (
<View>
<Button title='-' onPress={decrement} />
<Text>{count}</Text>
<Button title='+' onPress={increment} />
</View>
);
};
Keep in mind that the selection of a state management library is often influenced by your project’s particular requirements, team preferences, and the architecture of your application.
While Zustand is an excellent option for numerous applications, alternative state management solutions exist within the React ecosystem, including Redux, MobX or React’s native useContext and useReducer. Opt for the one that best aligns with your project’s demands and your team’s proficiency.
Navigation: Expo Router
The Expo Router library brings file-based routing to React Native. If you have had experience with routing in Next.js, you will find your way here quickly.
With Expo Router, you can quickly and effectively organize the navigation structure in your application. This means creating new screens and navigating between them has become easier than ever.
Core features of Expo Router:
- Linking and dynamic and static routing in Next.js style
- Shareable: Each screen within your application is inherently capable of deep linking, allowing any route in your app to be shared via links.
- Offline-first: Applications are stored in the cache and operate with offline-first functionality, ensuring automatic updates upon publishing a new version.
Optimized: Routes are automatically optimized with lazy-evaluation in production and deferred bundling in development.
How file-based-routing works? When a file is generated within the app directory, it spontaneously transforms into a route within the application. To illustrate, the ensuing files will generate the corresponding routes:app/index.tsx matches /
app/homescreen.tsx matches /homescreen
app/settings/index.tsx matches /settings
app/article/[slug].tsx matches dynamic paths like /article/1 or /article/2
Expo Router vs React Native Navigation
Expo Router and React Native Navigation are two widely used navigation solutions for React Native applications. Despite their common goal of overseeing navigation within an application, they have apparent differences.
React Native Navigation is a native navigation library for React Native apps. It is designed to provide a smooth and performant navigation experience by using native navigation components instead of JavaScript-based solutions. React Native Navigation is still great but it can become increasingly complex and almost unmanageable in larger projects.
That’s why you should have a look at Expo Navigation. Expo Router provides an expressive API along with a range of customizable navigators, such as Stack Navigator, Tab Navigator, etc. It presents a straightforward and user-friendly approach to outlining navigation sequences and screens through components and props.
Expo Router streamlines app development, offering a quicker and more intuitive experience for developers. Deep linking is seamlessly integrated, and Expo Router naturally adheres to URL-based conventions. Expo Router is much easier to use. So for new projects, I recommend using Expo Router instead of React Native Navigation.
Testing framework – Jest
Jest stands as a JavaScript testing library crafted for the examination of JavaScript code, with a specific emphasis on React applications. Embraced extensively within the React and broader JavaScript ecosystem, Jest is favoured for crafting unit tests, integration tests, and end-to-end tests. Originating from Facebook’s development, it prioritizes simplicity and speed in the testing process.
Jest boasts a range of notable features:
- Simplified Configuration: Jest is designed for hassle-free testing setups with minimal configuration.
- Snapshot Testing: Introducing snapshot testing, Jest captures the output of components or functions, preserving it as a snapshot. Subsequent test runs compare output to the saved snapshot, aiding in the identification of unintended changes.
- Code Coverage Analysis: Jest includes built-in capabilities for code coverage analysis, generating reports that reveal the extent to which tests cover the codebase.
- Built-in Mocking Support: Jest offers built-in support for mocking, enabling developers to effortlessly mock modules and functions for isolated unit testing.
Below is an example unit test for the Button component in which we want to check whether:
- The title is displayed correctly
- onPress works properly
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import Button from '~/components/atoms/Button';
it('renders title correctly', () => {
const { getByText } = render(<Button title="Press" />);
const buttonElement = getByText('Press');
expect(buttonElement).toBeTruthy();
});
it('should call onPress function when pressed', () => {
const onPressMock = jest.fn();
const { getByText } = render(<Button title="Press" onPress={onPressMock} />);
const buttonElement = getByText('Press');
fireEvent.press(buttonElement);
expect(onPressMock).toHaveBeenCalled();
});
Jest is frequently paired with tools like React Testing Library, proving itself a preferred choice for React Native testing in a variety of projects.
Data Fetching – React Query
React Query stands as a React library for data fetching and state management. It furnishes a collection of hooks that empower you to fetch, cache, and update data in your React Native applications, all without the necessity of engaging with global state management libraries.
Undoubtedly, React Query is as a top-tier library for managing server state. Its out-of-the-box functionality, coupled with a zero-config approach, makes it an excellent choice. Moreover, React Query offers customization options to suit your preferences as your application evolves.
With React Query, you gain the upper hand in tackling the complicated challenges of server state. It gives you control of your app data proactively, preventing it from taking control of your application dynamics.
Key features of React Query:
- Automated Caching: React Query seamlessly caches your data and automatically synchronizes it in the background upon component mounting or updates.
- Pagination and Infinite Queries: React Query effortlessly supports intricate asynchronous patterns, including pagination and infinite loading, right out of the box.
- Error handling: Handling errors with react query is easy
- Offline Support: Very helpful when you have offline mode in your application
- Background Updates: The library enables automatic background data retrieval, allowing you to keep the user interface up to date even when the application is running in the background or the device is idle.
- Documentation and Active Community: React Query has extensive documentation and an active community, making it easy to find help and solve problems. React Query is demonstrated in its fundamental and straightforward configuration in the provided example.
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/react-query';
import { ActivityIndicator, Text, View } from 'react-native';
const queryClient = new QueryClient();
const Card = () => {
const API_URL = 'https://api.github.com/repos/TanStack/query';
const { data, isLoading, isError } = useQuery({
queryKey: ['repoData'],
queryFn: () => fetch(API_URL).then((res) => res.json()),
});
if (isError) return <Text>Error Message</Text>;
return (
<View>
{isLoading ? (
<ActivityIndicator />
) : (
<View>
<Text>{data.name}</Text>
<Text>{data.descritpion}</Text>
</View>
)}
</View>
);
};
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Card />
</QueryClientProvider>
);
}
Data Storage – MMKV
MMKV, crafted by WeChat, is an open-source library dedicated to key-value storage. Tailored for mobile applications, it prioritizes efficiency and swift data storage. The acronym “MMKV” signifies its specialization in “Multi-Process, Multi-Thread Key-Value” storage.
For those seeking a data storage library in 2024, MMKV emerges as the optimal choice due to its speed, efficiency, and adaptability to the rigorous storage demands of mobile applications. It proves especially beneficial in scenarios necessitating swift and concurrent access to key-value pairs.
Key features of MMKV include:
- Cross-Platform Support: MMKV is engineered for effortless cross-platform compatibility, rendering it applicable for integration into both Android and iOS applications.
- Data Encryption: MMKV incorporates data encryption, enhancing the security of sensitive information stored within the library by adding an extra layer of protection.
- Efficiency: MMKV is finely tuned for efficiency, with a focus on delivering swift and dependable key-value storage while minimizing any unnecessary overhead.
- Memory Mapping: MMKV leverages memory mapping techniques to boost data access speed. This approach enables direct loading of data into memory, thereby diminishing the necessity for repetitive disk access.
- Multi-Thread Support: MMKV is constructed to manage data storage seamlessly in environments with multiple processes and threads. This adaptability renders it well-suited for applications that necessitate concurrent access to data.
- Lightweight: MMKV is crafted to be lightweight with minimal overhead, aligning it perfectly with the resource efficiency requirements of mobile applications.
UI – Tailwind (Native Wind) + clsx
Native Wind is a CSS framework for building custom user interfaces in React Native. It’s inspired by Tailwind CSS, a popular utility-first CSS framework for web development.
The utility-first approach promotes the idea of composing your UI by using many small utility classes that serve a single purpose, such as setting a colour, defining a margin, or applying a border. This approach can lead to more maintainable code by reducing the amount of CSS you have to write and making it easier to understand what a piece of UI will look like directly from the markup.
Native Wind provides a set of utility classes that you can use in your React Native components. These classes are named and function similarly to those in Tailwind CSS, making it easier for developers familiar with Tailwind to transition to React Native development.
Main features of Native Wind
- Works across all React Native platforms, adopting the optimal style system tailored for each platform.
- Leverages the Tailwind CSS compiler for efficient styling.
- Computes styles during build time, contributing to faster component performance.
- Includes a Babel plugin for straightforward setup
- Incorporates a small runtime to uphold component speed.
- Respect all settings in tailwind.config.js, encompassing themes, custom values, and plugins.
- Applies pseudo-classes (hover, focus, active) on compatible components.
- Supports features such as dark mode, arbitrary classes, and media queries.
- Enables styling based on parent state, automatically adjusting children’s styles in response to parent pseudo-classes. When working with Native Wind, leveraging a library to generate classes based on conditions is advisable. One excellent option is clsx. Widely adopted in the React ecosystem, CLSX proves valuable for dynamically handling CSS classes based on logical conditions.
The core concept of CLSX involves passing various arguments and subsequently generating a CSS class according to these arguments. This proves beneficial when there’s a need to conditionally include or exclude classes based on specific conditions in our code.
Example clsx usage below:
import clsx from 'clsx';
import { Text } from 'react-native';
export const Typography: React.FC = ({
isLight,
isRegular,
isBold,
children,
}) => (
<Text
className={clsx('font-sans', {
'font-light': isLight,
'font-regular': isRegular,
'font-bold': isBold,
})}
>
{children}
</Text>
);
When it comes to new projects, developers often have to decide between Native Wind and Styled Components as styling tools. In my opinion, Native Wind is a much better choice. Creating components is much easier and faster and the code is more readable.
Below is a comparison of a simple button component built with Native Wind and Styled Components:
STYLED COMPONENTS
import styled from 'styled-components/native';
const StyledButton = styled.Pressable`
opacity: ${({ disabled }) => (disabled ? 0.7 : 1)};
flex-direction: row;
background-color: red;
padding: 16px;
;
const StyledButtonText = styled.Text`
font-size: 16px;
color: white;
text-align: center;
;
const StyledButtonIconBox = styled.View`
margin-right: 8px;
;
export const Button: React.FC<ButtonProps> = ({
icon: Icon,
disabled,
children,
…rest
}) => (
<StyledButton {...{ disabled, …rest }}>
{Icon && (
<StyledButtonIconBox>
<Icon />
</StyledButtonIconBox>
)}
<StyledButtonText>{children}</StyledButtonText>
</StyledButton>
);
NATIVE WIND EXAMPLE
export const Button: React.FC<ButtonProps> = ({
icon: Icon,
disabled,
children,
...rest
}) => (
<Pressable
{...{ disabled, ...rest }}
className={clsx('flex-row bg-red-600 p-4', {
'opacity-70': disabled,
})}
>
{Icon && (
<View className='mr-2'>
<Icon />
</View>
)}
<Text className='text-center text-xs text-white'>{children}</Text>
</Pressable>
);
As you can see, we wrote the same component but in less time and with less code. The code is also more readable because it looks more like HTML. The example presented is very simple, but imagine how big a difference in implementation would be in the case of large components consisting of a larger number of batches. In such a situation, the Styled Components code becomes less readable.
Animations – React Native Reanimated 3
Reanimated, a JavaScript library intricately linked with React Native, excels in crafting intricate and high-performance animations. As the second iteration of the Reanimated library, it is distinguished by its declarative API, renowned for seamlessly managing intricate animations and gestures with finesse.
When it comes to animations, Reanimated is still second to none. So if your application contains animations and you want to encode them relatively easily to make them smooth, there is only one choice – React Native Reanimated library.
Key features of Reanimated 2 include
- High Performance: A core objective of Reanimated is to provide high-performance animations. It accomplishes this by transferring animations to the native UI thread, yielding interfaces that are not only smoother but also more responsive.
- Support for Gestures: Reanimated offers integrated support for managing gestures, simplifying the process of crafting interactive and gesture-driven animations within React Native applications..
- Native Driver Integration: It effortlessly integrates with the native driver in React Native, enhancing animation performance by harnessing the capabilities of the native platform underneath.
Application monitoring – Sentry / BugSnag
There are quite a lot of application monitoring tools and it is difficult to choose the best one. I can only recommend the tools that I had the best experience working with last year.
Sentry and Bugsnag are tools for monitoring bugs and reporting problems in applications. Both tools are designed to help developers identify, track and fix bugs in real-time, helping to improve software quality.
Both of these tools are useful for development teams to reduce error response time, understand their source and effects, and ultimately improve software quality.
The choice between Sentry and Bugsnag may depend on the team’s preferences, the specifics of the project, and the features and integrations each tool offers.
Below are the main functions of the tools:
- Real-time bug tracking.
- Collecting diagnostic data such as variable values, system information, etc.
- Integrations with various tools and frameworks.
- Notifications about errors and their priorities.
- Debugging with Expo
- Debugging a React Native app involves identifying, analyzing, and resolving errors, issues, and unusual behaviour in a mobile app written using the React Native framework. It is crucial in the software development process because it allows developers to track, understand, and fix errors in the code.
Various tools and development environments are available for debugging React Native applications.
Below are some popular debugging tools in the context of React Native:
- React Native Debugger
- Reactotron
- Flipper
- Expo Debugging
All of the above work and choosing a specific one depends mainly on your preferences. But if you are using Expo in your project, I recommend debugging with Expo.
Hermes has been established as the primary JavaScript engine, taking over the default position from JavaScriptCore (JSC) in SDK 48, as previously communicated in SDK 47.
The debugging experience has undergone a significant enhancement compared to JSC. “Open JS Debugger” in apps with Hermes enabled will open the Hermes inspector in Chrome DevTools. Hermes inspector supports the New Architecture, unlike debugging remotely in Chrome with JSC.
With the release of Expo SDK 49 – network debugging is available in the JS debugger.
In summary, I think there is no need to use external tools since the application can be fully debugged using the built-in debugger.
TRENDS AND INNOVATIONS: FOCUSING ON EXPO
Expo is a set of libraries, services and tools that lets you build native applications for iOS and Android. It’s built around the React Native framework. It makes it easy to develop JavaScript and React Native mobile apps by eliminating some of the usual difficulties associated with the setup and build process. Here is a examination of Expo’s role in React Native development:
- Simplified Setup: Expo provides a streamlined setup process for new React Native projects. Expo provides tools and a platform that make it easier for developers to start building mobile applications without the need for complex native development configurations.
- Expo CLI: This is a command line tool that helps in creating a new project, running the project and publishing.
- Expo SDK: The Expo SDK is a set of libraries that provide access to the device and system functionality like camera, maps, notifications and more. You don’t need to write any native code because these are all JS APIs.
- Expo Client App: This is a mobile app provided by Expo that can be used to run your apps during development. It allows you to test your app on a device without having to create a build.
- Over-the-Air Updates (OTA): This is a powerful feature for pushing fixes and updates quickly.
- Managed Workflow: With Expo’s managed workflow, you don’t need to touch any native code or setup. Expo handles all of this for you. Remember, while Expo provides a lot of benefits, it might not be suitable for all projects. If you need a specific native library that’s not included in the Expo SDK, you might need to eject from Expo and use plain React Native.
CONCLUSION
So, after all, to choose the right tech stack for your mobile app, you need to consider all of the business and technical functionality and features you want to implement into your app. If you’ve decided to build a cross-platform mobile app, choosing React Native development is a great choice, since it gives you many advantages and lets you reuse the once-built components across various projects.
Today we’ve discovered a modern technology stack for mobile apps built with React Native we, as experienced React Native developers use across our projects. Of course, when we choose the best tech stack for mobile projects for our customers, we don’t just follow the trends, we debate with the customer about their requirements, possible previous mobile app projects, and their business goals and then choose the right development tools. But this guide may lead you through the complexities of a modern stack and help you choose the best technology for yourself.
If you have any questions or own suggestions for the perfect tech stack, let us know in the comment section.