4

Styling, Storage, and Navigation in React Native

Now that you know the general concepts behind React Native, it’s time to have a deeper look at the most common areas of React Native.

This chapter covers different areas, all of which are important when working with React Native. When creating a large app with React Native, you always have to have a good understanding of how the styling of your app works to create a beautiful product. Besides styling, there is another thing that decides if users will like your app from an aesthetic point of view – animation. However, this will be covered in Chapter 6, Working with Animations.

Another thing we will focus on in this chapter is how to store data locally on users’ devices. Every platform works differently. While Android and iOS are quite similar and you can get access to the device’s storage with huge capacity, this is completely different when working with the web, where capacity is very limited.

The last thing we’ll cover is how to navigate between screens in your React Native app. Again, this can differ from platform to platform, but you’ll get a good overview of the different navigation concepts.

In this chapter, we will cover the following topics:

  • Understanding how to style a React Native app
  • Using local storage solutions in React Native
  • Understanding navigation in React Native

Technical requirements

To be able to run the code in this chapter, you must set up the following:

Understanding how to style a React Native app

You can choose from different solutions to handle styling in your React Native app. But before we take a look at the most common ones, you must understand the underlying concepts. The first thing we’ll cover in this chapter is what all these solutions try to achieve.

Make styling maintainable

Styling is often handled very poorly when a project starts because it does not interfere with the business logic, so it isn’t likely to introduce bugs. So, most of the time, when thinking about the architecture of an application, most developers think of state management, data flow, component structure, and more, but not about styling. This always takes its toll when a project grows. It starts to take more and more time to keep a consistent design and making changes to your UI becomes a real pain.

Therefore, you should think about how to handle styling in your application right at the beginning. No matter what solution or library you use, you should always stick to the following concepts:

  • Use a central file for colors, fonts, and sizes: This should either be a single file or one file for colors, one for fonts, and one for sizes such as margins, paddings, and border radiuses. I prefer to use a single file.
  • Never hardcode values in your components/CSS files: You should never use fixed values inside your component. Always use the values you define in your central file. This guarantees that your UI stays consistent and that you can easily change the values if you have to adapt.
  • Never duplicate code: When you catch yourself copying the styling of parts of a component because it’s easier, faster, or more convenient, always keep in mind that it isn’t in the long run. Duplicate code always leads to inconsistencies in the UI and makes you have to touch multiple files when you want to change something later. So, instead of copying and pasting your code, extract it to a component or styles file. You will learn more about these options later.

When we come back to our example project with these concepts, we will have to refactor it because, at the moment, we violate all of these concepts. We have no central file; we have hardcoded values everywhere and we have a backButton style defined in multiple files.

First, let’s create a central file to store our values. This could look like this:

import {Appearance} from 'react-native';
const isDarkMode = Appearance.getColorScheme() === 'dark';
const FontConstants = {
  familyRegular: 'sans-serif',
  sizeTitle: 18,
  sizeRegular: 14,
  weightBold: 'bold',
};
const ColorConstants = {
  background: isDarkMode ? '#333333' : '#efefef',
  backgroundMedium: isDarkMode ? '#666666' : '#dddddd',
  font: isDarkMode ? '#eeeeee' : '#222222',
};
const SizeConstants = {
  paddingSmall: 2,
  paddingRegular: 8,
  paddingLarge: 16,
  borderRadius: 8,
};
export {FontConstants, ColorConstants, SizeConstants};

As you can see, we have all our values in one single place. If you take a deeper look, we also introduced dark mode to our app, which was a 3-minute task with our central color store. We only have to get the information about the device appearance settings and deliver the colors accordingly.

Note

You can test your app in dark mode very easily on the iOS Simulator. Go to Settings, scroll to the bottom, and choose Developer. The Developer screen will open; the first toggle activates Dark Appearance. If you support dark mode with our app, you should always test on two simulators – one in the dark mode and one in the light mode.

Now that we have our central store, let’s create a <BackButton /> component to get rid of the duplicated style definitions. This can look like this:

interface BackButtonProps{
  text: string;
  onPress: () => void;
}
const BackButton = (props: BackButtonProps) => {
  return (
    <Pressable onPress={props.onPress}
      style={styles.backButton}>
      <Text>{props.text}</Text>
    </Pressable>
  );
};
const styles = StyleSheet.create({
  backButton: {
    padding: SizeConstants.paddingLarge,
    marginBottom: SizeConstants.paddingLarge,
    backgroundColor: ColorConstants.backgroundMedium,
  },
});

In our newly created component, we don’t use fixed values anymore, but we are referencing the values in our central store.

Lastly, we have to go through our app, replace the backButton pressables with our new component, and replace the fixed values with references to our central store. With that, we have complied with the concepts.

These concepts are the core of different libraries or solutions. To choose the right solution for your project, one of the most important decisions is which platform to deploy to. The following subsection will cover the most common solutions, including information about which platform the solution works best on.

Choosing the right styling solution

In this subsection, we’ll have a look at inline styling, React Native StyleSheets, CSS modules, and styled-components. All four solutions work well and have their benefits and drawbacks. We’ll start with inline styles.

Using React Native inline styles

To understand inline styles, let’s have a look at a code example. The following code shows the <Header /> component from our example project from the previous chapter but it uses inline styles to style the Text component:

const Header = (props: HeaderProps) => {
  return <Text style={{
          fontSize: 18,
          fontWeight: 'bold',
          marginBottom: 16}
        }>
            {props.text}
        </Text>;
};

As you can see, we can just create an object with the styling rules. This works and has a big advantage. Not only can you use fixed values, but you can also use any static or dynamic value you can access in your component. This can be very useful, especially when you are working with user-defined themes. But this approach also comes with multiple disadvantages.

First, the code gets quite confusing when the project grows – at least, I think the code is hard to read when styling, components, and data are mixed in that way. So, I would always prefer to separate this as much as possible.

Next, you cannot reuse any styling. You must copy your styles every time you need them again. Now, you could argue that you wouldn’t have to copy the styles because you can simply extract the component that includes the style into a custom component. Although this is correct, there are some cases where you don’t want to do this. We’ll have a deeper look at these scenarios in the next subsection.

Next, we must think about performance. Inline style objects will be recreated at every render, which can negatively impact the performance and memory usage of your app.

Last, we’ll have a look at the different platforms. This inline style approach has very little room for optimization on build time for the different platforms. While this may be no real problem on Android, iOS, Windows, and macOS, it can become a real pain for the web because it makes your bundle size a lot larger.

On the web, you must think about load times a lot because the user has no installed version of your application. Also, search engines such as Google care a lot about load times, and it will affect your ranking positively or negatively. So, your styling code must be optimized during the build process, which is not possible with inline styles.

To take advantage of optimization, you’ll have to use StyleSheets. We’ll have a look at them next.

Using React Native StyleSheets

We used StyleSheets in our example app in the previous chapter, but we’ll have a look at them again here to truly understand their benefits. Not only do they make the code more readable and support a good separation of styling and business logic, but they also make it possible to use a lot of performance optimization at the build time and runtime of your app.

The following code is for the <Header /> component from our example app. It uses React Native StyleSheets for styling:

const Header = (props: HeaderProps) => {
  return <Text style={styles.title}>{props.text}</Text>;
};
const styles = StyleSheet.create({
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 16,
  },
});

There are multiple things you should realize when looking at this code:

  • First, it is much clearer and better separated.
  • Second, StyleSheet is defined outside of the component, which makes it persist between rerenders. This is better in terms of performance and memory usage.
  • Third, StyleSheet.create will create errors in your simulator when you are using styles that can’t be interpreted. This can be very useful for catching bugs at a very early stage.

But the biggest benefit of StyleSheets is the possibility to optimize your styling code for the web. The open source web library known as react-native-web does a great job of splitting all the StyleSheets of your application into classes and adding the needed class names to your components. This makes your code small and optimized and improves your load time a lot.

Besides all these benefits, there is one problem with StyleSheets. Since they are declared outside of your component, you cannot access your component variables, such as state and props. This means that if you want to use a user-generated value in your styling, you have to combine your StyleSheet values with inline styles, like this:

<Text style={[styles.title, {color:props.color}]}>{props.text}</Text>

This code would use the title style from the StyleSheet and add a user-defined color to the <Text /> component. This combined approach can also be used when working with animations. You can read more about this in Chapter 6, Working with Animations.

Last, we’ll have a look at another benefit of StyleSheets. You can use a style multiple times in your component. Again, if you stick to my recommendations, you will never have to do that because you will be creating a custom component in these scenarios. But for daily work, there are circumstances where it is faster to not create a component and where it also does not hurt.

For example, if you have a simple component with two lines of text, you can either create a <TextLine /> component and use it two times, or simply use two <Text /> components with the same style reference in a StyleSheet.

This first approach with the <TextLine /> component is the cleaner one, but the second approach will save you some time and does not create problems in the long run. So, in this case, StyleSheets have another benefit versus inline styles.

Note

Always be careful when you use the same style multiple times. While it can be useful, in many cases, you duplicate code that should be extracted into a custom component.

Now that we understand this built-in solution, let’s look at two solutions that need external libraries.

Styling with CSS modules

CSS modules are very popular on the web. You use CSS, Sass, or Less to style your components. In most cases, you would end up having one additional style file per component. Experts often argue if that’s good or bad.

You have an additional file, but you have a clear separation between styling and components. I do like the separation, but if you manage to split your application into small components, adding the style directly to the component is fine from my point of view.

Using CSS modules in React Native needs some additional configuration. Since React Native does not have a built-in CSS processor, you must transform your CSS code into JavaScript styles before it can be displayed. This can be done with the babel transformer.

CSS modules can be a great choice if you share your styles between React (the web) and React Native projects, without using react-native-web to generate the web part. This is especially true when you are building an app for an existing web application.

One very important problem with this approach is that you can’t use your JavaScript variables in your CSS modules. Even though you can create and use CSS variables, this does not enable you to use user-generated values in your styles.

If you start a green field project for Android, iOS, Windows, or Mac, I wouldn’t recommend using CSS modules since, for these platforms, the CSS module approach has no benefits over StyleSheets. Again, the only scenario where I would recommend using CSS modules is when you build an app for an existing web application that is based on CSS modules.

There is also another solution that is very popular for React web projects that can be used in React Native. It’s called styled-components and you’ll learn about it in the next subsection.

Understanding styled-components

styled-components is a very popular library for styling React web applications. It also has very good support for React Native and can be a good choice in some cases. The styled-component approach uses the component approach through to the end. Instead of styling the primitive components such as View and Text, you enhance them with tagged template literals to create new components, called styled-components.

The following code shows the <Header /> component in our example project but styled with styled-components:

import styled from 'styled-components/native';
const Header = (props: HeaderProps) => {
  return <StyledText>{props.text}</StyledText>;
};
const StyledText = styled.Text`
  font-size: ${FontConstants.sizeTitle};
  font-weight: ${FontConstants.weightBold};
  margin-bottom: ${SizeConstants.paddingLarge};
  color: ${ColorConstants.font};
`;

As you can see, we create the StyledText component by using styled from styled-components and add a template literal to the React Native Text component. Inside this literal, we can write plain CSS. The cool thing here is that we can also use JavaScript variables and we can even pass props to our styled-component. This would look like this:

<StyledText primary>{props.text}</StyledText>;

This is how we would pass a property to our StyledText component. Now, we can use this property inside our template literal:

const StyledText = styled.Text`
  font-size: ${props => props.primary ?
                          FontConstants.sizeTitle :
                            FontConstants.sizeRegular};
`;

This function is called interpolation and makes it possible to use user-generated content inside the CSS of our styled-components.

This is awesome because it solves a lot of problems, supports a clear separation between structure and styling, and allows us to use regular CSS, which is more familiar to most developers than the camel-cased CSS in the JavaScript of StyleSheets.

While I like this approach for the web, I remain critical of it for app-only projects. The styled-components library has a lot of useful optimization features for the web, but on pure React Native projects, it also compiles to CSS in JavaScript styles. In addition, it doesn’t provide support for animations, which is a very important part of modern apps. You can read more about this in Chapter 6, Working with Animations.

Although I wouldn’t recommend using styled-components for pure React Native projects, they can be very useful when you try to share your styling code between React Native and React projects, without using react-native-web. In this case, you can benefit from styled-components a lot.

If you want to have a deeper look at styled-components, I recommend reading the official documentation at https://styled-components.com/docs.

In this section, we learned the most important concepts of styling your React Native app and looked at the most common solutions to implement the styles. Most of the time, you wouldn’t write all your styles on your own but use a UI library. This will be handled in Chapter 9, Essential Tools for Improving React Native Development.

If you want to see all changes to the example project, please have a look at the repository for this example project and choose the chapter-4-styling tag.

Now that we know how to style our app, it’s time to store some data on the user’s device.

Using local storage solutions in React Native

Storing data locally is a very important task in mobile apps. Even nowadays, you cannot be sure that a mobile device is always connected to the internet. Because of this, it is best practice to create your app in such a way that it has as much functionality as possible, even without a connection to the internet. That said, you can see why storing data locally is important for React Native apps.

The most important criterion for differentiation for local storage solutions is if it is a secure or an unsecure storage solution. Since most apps store at least some information about the user, you should always think about which information you want to put in which store.

Important

Always use a secure storage solution to store sensitive information.

While it is important to store sensitive data in a secure store, most data, such as user progress, app content, and more, can be stored in a normal storage solution. Secure storage operations always come with some overhead due to encryption/decryption and/or accessing special device functionalities, so you should only use them for sensitive information to prevent a negative impact on your app’s performance.

In the following subsection, you will learn about the most common storage solutions for normal data.

Storing non-sensitive data

For a long time, React Native shipped with its built-in storage solution called AsyncStorage. But since the React Native core team at Facebook tried to reduce the React Native core to the minimum (lean core), AsyncStorage was handed over to the community for further development.

Nevertheless, it is very well maintained and most likely the most used storage solution. Besides AsyncStorage, other common solutions include react-native-mmkv/react-native-mmkv-storage, react-native-sqlite-storage/react-native-quick-sqlite and react-native-fs. All these solutions have their strengths and weaknesses, work completely differently, and can be used for slightly different tasks. Let’s start with the most popular one.

Working with AsyncStorage

AsyncStorage is a simple key/value store that can be used to store data. While it can only store primitive data, you must serialize complex objects to JSON before storing them. Nevertheless, it is very simple to use. The API looks like this:

import AsyncStorage from '@react-native-async-storage/async-storage';
// set item
const jsonValue = JSON.stringify(value)
await AsyncStorage.setItem('@key', jsonValue)
// get item
const strValue = await AsyncStorage.getItem('@key')
const jsonValue = strValue != null ? JSON.parse(strValue) : null

As you can see, there are very simple APIs for setting and getting data.

AsyncStorage is not encrypted and cannot be used to run complex queries. It is a simple key/value store; there is no database. Also, it does not support transactions or locking. This means you have to be extremely careful when you write/read to/from different parts of your application.

I recommend using it to store user progress, information about app content, and any other data that does not have to be searchable. For more information on installing and using AsyncStorage, please look at the official documentation at https://react-native-async-storage.github.io/async-storage/docs/install/.

A relatively new alternative to AsyncStorage is MMKV for React Native. It is up to 30 times faster and comes with a lot more features.

Working with MMKV in React Native

MMKV is a native storage solution developed by WeChat and used in their production app. There are multiple React Native wrappers for this native solution; most of them are already based on JSI and therefore support synchronous and super-fast access.

Like AsyncStorage, MMKV is a simple key/value store. This means complex objects must be serialized to JSON strings before they can be stored. The API is nearly as simple as AsyncStorage:

import { MMKV } from 'react-native-mmkv'
export const storage = new MMKV()
// set data
const jsonValue = JSON.stringify(value)
storage.set('@key', jsonValue)
// get data
const strValue = storage.getString('@key')
const jsonValue = strValue!= null ? JSON.parse(strValue) : null

As you can see, thanks to JSI, the API is synchronous, so we don’t need to work with async/await syntax. In the second line, you can see the initialization of the store. This is one advantage over AsyncStorage because you can work with multiple instances of MMKV stores.

While it is possible to encrypt data with MMKV, at the time of writing, there is no secure solution regarding how to handle the key. Therefore, I would only recommend using it to store non-sensitive data. This may change in the future.

MMKV can be used as a faster drop-in replacement for AsyncStorage. The only disadvantage MMKV has compared to AsyncStorage is that the React Native wrappers are not used that much at the time of writing. There are two well-maintained React Native MMKV mappers, so you should have a look at them when you consider using MMKV for your project. You can find more information about installation, usage, and APIs there. The first one is react-native-mmkv. It is a leaner project and comes with a simpler API. It’s also much simpler to install. You can have a look at it here: https://github.com/mrousavy/react-native-mmkv. The second one is react-native-mmkv-storage. It provides more features, such as indexing and data life cycle methods, which can be very useful when it comes to locking and transactions. You can have a look at it here: https://github.com/ammarahm-ed/react-native-mmkv-storage.

Now that we’ve looked at AsyncStorage and MMKV, which handle very similar use cases, let’s look at a solution that comes with some more features: SQLite.

Working with SQLite

Compared to AsyncStorage and MMKV, SQLite is not only a simple key/value store – it is a complete database engine that includes functionalities such as locking, transactions, and advanced querying.

However, this means you can’t simply store your objects as serialized data. SQLite uses SQL queries and tables to store your data, which means you have to process your objects. To insert data, you must create a table with a column for each property and then insert every object with a SQL statement. Let’s have a look at the following code:

import { QuickSQLite } from 'react-native-quick-sqlite';
const dbOpenResult = QuickSQLite.open('myDB', 'databases');
// set data
let { status, rowsAffected } = QuickSQLite.executeSql(
  'myDB',
  'UPDATE users SET name = ? where userId = ?',
  ['John', 1]
);
if (!status) {
  console.log(`Update affected ${rowsAffected} rows`);
}
// get data
let { status, rows } = QuickSQLite.executeSql(
  'myDB',
  'SELECT name FROM users'
);
if (!status) {
  rows.forEach((row) => {
    console.log(row);
  });
}

As you can see, it takes much more code to insert and query data. You need to create and execute SQL and process the data you get to have it in a format you can work with. This means that SQLite isn’t as easy and fast to use as AsyncStorage and MMKV, but it comes with advanced querying features. This means that you can filter and search your data and even join different tables.

I would recommend using SQLite if you have very complex data structures, where you need to join and query different objects or tables a lot. I prefer simpler solutions for local data storage, but there are use cases where SQLite is the better fit.

Besides the higher complexity of using it, SQLite also adds some MB to your app size because it adds its SQLite database engine implementation to your app.

The most used React Native wrapper for SQLite is react-native-sqlite-storage. The API is simple, and it is used in a lot of projects. You can learn more about it at https://github.com/andpor/react-native-sqlite-storage.

Another solution is react-native-quick-sqlite. It is a relatively new library, but it is based on JSI and therefore up to five times as fast as other solutions. You can learn more about it at https://github.com/ospfranco/react-native-quick-sqlite.

Now that you’ve learned about the SQLite database engine, let’s look at another use case. Sometimes, you have to store large amounts of data, which means you need direct access to the filesystem. This is what we’ll explore next.

Using the filesystem with React Native

To store large amounts of data, it is always a good idea to create and store files. On iOS and Android, every app runs in a sandbox that no other app has access to. While that does not mean that all your files are secure – they can be retrieved by the user quite easily – it gives you at least some level of privacy regarding your data. However, this sandbox mode means that you cannot access the data of other apps.

To read and write data to your app’s sandbox in React Native, you can use libraries such as react-native-fs. This library provides constants with the paths you have access to and lets you read and write files from the filesystem.

I recommend using this approach when you’re synchronizing files from a server or writing large amounts of data. Most of the time, you can combine this approach with one of the previous approaches to store files locally and then store the path of the file in one of the other storage solutions.

If you want to find out more about filesystem access on React Native, please have a look at the documentation of react-native-fs at https://github.com/itinance/react-native-fs.

With that, we’ve covered the most common solutions for storing and accessing non-sensitive data. This is where you should store most of your data. However, some data contains sensitive information such as passwords or other user information. This data needs another level of protection. So, let’s have a look at some storage solutions for sensitive information in React Native.

Storing sensitive data

When you store sensitive information on the device of a user, you should always think about how to secure it. Most of the time, this will be irrelevant, but when the user loses the device, you should make sure that their sensitive information is as secure as possible.

You will never be able to ensure 100% data security when you have no control over the device. However, we need to do the best we can to make it as hard as possible for that sensitive information to be retrieved.

The first thing you should consider is if it is necessary to persist the information. Information that is not there cannot be stolen. If you need to persist information, use secure storage. Android and iOS provide built-in solutions for securely storing data. React Native provides wrappers for these native built-in solutions. The following ones are well maintained and can be used with ease:

  • expo-secure-store: Uses iOS Keychain and Android SharedPreferences combined with Keystore System. It provides an easy API and can store values up to 2,048 bytes in size. More information can be found at https://docs.expo.dev/versions/latest/sdk/securestore/.
  • react-native-sensitive-info: This library is very well maintained and provides a lot of functionality. It also adds another layer of security, which protects your data even on rooted devices. It supports Android, iOS, and Windows. More information can be found at https://mcodex.dev/react-native-sensitive-info/.
  • react-native-keychain: This is another well-maintained library with an easy API. It supports Android and iOS and encrypts data on all devices. More information can be found at https://github.com/oblador/react-native-keychain.

Again, even if these solutions are very good and secure, based on native implementations, there will never be 100% security for data. So, please only persist necessary.

Now that you learned about data storage solutions and the difference between sensitive and non-sensitive data, it’s time to look at navigation in React Native apps.

Understanding navigation in React Native

React Native does not come with a built-in navigation solution. That’s why we worked with a global state and simply switched components while navigating in our example app. While this works technically, it does not provide a great user experience.

Modern navigation solutions include performance optimization, animations, integration in global state management solutions, and much more. Before we dive deep into these solutions, let’s see what navigation looks like on different platforms.

Navigating different platforms

If you open any iOS or Android app, you’ll soon realize that navigation in an app is completely different from navigating the web in a browser. A browser navigates from page to page by replacing the old page with the new one. In addition to that, every page has a URL and can be accessed directly if it’s typed in the browser’s address bar.

In an iOS or Android app, navigation takes the form of a combination of different navigators. The page you navigate away from doesn’t always get replaced by the new one. Multiple pages can be active at the same time.

Let’s have a look at the most common navigation scenarios and navigators to handle these scenarios:

  • Stack navigator: When navigating to a new page in a stack navigator, the new page is pushed on top of the old page. Nevertheless, the old page doesn’t get unmounted. It continues to exist and if you leave the new page with a back button, you’ll automatically navigate back to the old page. The new page gets popped from the so-called layer stack, and you’ll find your old page in the same state you left it in. This also includes the scroll position.
  • Tab navigator: A very popular navigator is the tab navigator. This navigator provides up to five tabs that can be selected via a tab bar. This tab bar contains text and/or icons and can be on the top or at the bottom of the screen. Every tab has a layer stack. This means you can navigate every tab separately. The state of the tabs does not reset when you select another tab. In most cases, you simply have multiple stack navigators in your tab navigator.
  • Switch navigator: This navigator provides the same behavior as web navigation. When using this navigator, you’ll replace an old page or layer stack with the new one. This means the old page or layer stack gets unmounted and removed from memory. If you navigate back, the old page or layer stack will have a complete clean restart, as if you haven’t been there before.

Most apps combine these navigators to provide a great navigation experience to the user. Because this common navigation experience in mobile apps is so different from the web, you should always keep this in mind when planning a project for mobile and the web. You will learn more about this in Chapter 10, Structuring Large-Scale, Multi-Platform Projects.

Even though multiple community projects provide great support for navigation in React Native apps, such as react-native-navigation (supported by Wix; more information can be found at https://wix.github.io/react-native-navigation/docs/before-you-start/) and react-router/native (more information can be found at https://v5.reactrouter.com/native/guides/quick-start), we’ll focus on react-navigation in this section. It is by far the most commonly used, most actively maintained, and most advanced navigation solution for React Native.

Working with React Navigation

To understand how React Navigation works, it’s best to simply integrate it into our example project. We’ll do two things here. First, we’ll replace our global state navigation solution with a React Navigation Stack Navigator. Then, we’ll add a Tab Navigator to create a second tab, which we’ll use in the next chapter.

But before you can begin using React Navigation, you must install it. This process is easy – you just have to install the package and the dependencies via npm. This can be done with the npm install @react-navigation/native react-native-screens react-native-safe-area-context command. Since react-native-screens and react-native-safe-area-context have a native part, you’ll have to install the iOS Podfiles with the npx pod-install command. After this, you’ll have to create fresh builds to be able to use React Navigation. This can be done for iOS with npx react-native run-ios.

At the time of writing, some additional steps are necessary to get React Navigation to work on Android. Since this may change in the future, please have a look at the installation part of the official documentation at https://reactnavigation.org/docs/getting-started/#installation.

Now that have installed React Navigation, it’s time to use it in our example project. First, we’ll replace our global state-based navigation in App.tsx with a Stack Navigator. To use the Stack Navigator, we’ll have to install it using the npm install @react-navigation/native-stack command. Then, we can start using it in our app:

const MainStack = createNativeStackNavigator<MainStackParamList>();
const App = () => {
  return (
    <NavigationContainer>
      <MainStack.Navigator>
        <MainStack.Screen
          name="Home"
          component={Home}
          options={{title: 'Movie Genres'}}
        />
        <MainStack.Screen
          name="Genre"
          component={Genre}
          options={{title: 'Movies'}}
        />
        <MainStack.Screen
          name="Movie"
          component={Movie}
          options={({route}) =>
          ({title: route.params.movie.title})}
        />
      </MainStack.Navigator>
    </NavigationContainer>
  );
};

As you can see, our App.tsx got a lot simpler. We can remove all the useState hooks and all the setter functions because React Navigation will handle all this. All we need to do is create a Stack Navigator with React Navigation’s createNativeStackNavigator command and then return our Layer Stack in our return statement. Please note <NavigationContainer />, which is wrapping the entire application. This is necessary to be able to manage the navigation state and should usually wrap the root component.

Here, every screen has a name, a component, and some options. The name is also the key that the screen can be navigated to with. component is the component that should be mounted when the screen is navigated to. options allows us to configure things such as the header and the back button.

Now that we have defined the Layer Stack, it’s time to look at the views and see what has changed there. Let’s look at <GenreView />. This is where we can see all the changes best:

type GenreProps = NativeStackScreenProps<MainStackParamList, 'Genre'>;
const Genre = (props: GenreProps) => {
  const [movies, setMovies] = useState<IMovie[]>([]);
  useEffect(() => {
    if (typeof props.route.params.genre !== 'undefined') {
      setMovies(getMovieByGenreId(props.route.params.genre.
        id));
    }
  }, [props.route.params.genre]);
  return (
    <ScrollContainer>
      {movies.map(movie => {
        return (
          <Pressable
            onPress={() =>
              props.navigation.navigate('Movie',
                {movie: movie})}>
            <Text
             style={styles.movieTitle}>{movie.title}</Text>
          </Pressable>
        );
      })}
    </ScrollContainer>
  );
};

The first thing you can see is that there is another way to access the properties that are passed via React Navigation. Every component, which is a React Navigation screen, is passed two additional properties – navigation and route.

route contains information about the current route. The most important property of route is params. When navigating to a screen, we can pass params, which can then be retrieved through route.params. In this example, this is how we pass the genre to the view (props.route.params.genre), which we then use to fetch the movie list.

When you have a look at the onPress function of the <Pressable /> component in the return statement, you can see how to navigate to another page in React Navigation. The navigation property provides different functions to navigate between screens. In our case, we use the navigate function with the Movie key to navigate to the <Movie /> view. We also pass the current movie as a parameter.

When you compare the code to the example from the previous section, you’ll realize that the <Header /> and <BackButton /> components are missing. This is because React Navigation comes with built-in header and back button support. While you can disable this, its default behavior is for every screen to have a header, including a back button to the previous screen.

If you want to see all these changes, please have a look at the repository for this example project and choose the chapter-4-navigation tag.

If you run the example project on that tag, you’ll also see that React Native added animations to the navigation actions. These animations can be customized in any way possible. There is even a community library to support shared animated elements between the different pages. You can have a look at it here: https://github.com/IjzerenHein/react-navigation-shared-element.

Now that you’ve learned how to use the Stack Navigator, we’ll add another navigator. We want to create a second tab because we want to create an area where the user can save his favorite movies. This will be done with a Tab Navigator.

As with the Stack Navigator, we have to install the Tab Navigator before using it. This can be done with npm install @react-navigation/bottom-tabs. After we have installed the Tab Navigator, we can add it to our App.tsx. Please have a look at the following code snippet:

const MainStackScreen = () => {
  return (
    <MainStack.Navigator>
      <MainStack.Screen component={Home}/>
      <MainStack.Screen component={Genre}/>
      <MainStack.Screen component={Movie}/>
    </MainStack.Navigator>
  );
};
const App = () => {
  return (
    <NavigationContainer>
      <TabNavigator.Navigator>
        <TabNavigator.Screen
          name="Main"
          component={MainStackScreen}
          options={{
            headerShown: false,
          }}
        />
        <TabNavigator.Screen
          name="User"
          component={User}
        />
      </TabNavigator.Navigator>
    </NavigationContainer>
  );

This is a very limited example. To see the working code, please have a look at the example repository and choose the chapter-4-navigation-tabs tag. As you can see, we move the Main Stack to its own function component. Our App component now contains <TabNavigator /> with two screens.

The first screen gets <MainStackScreen /> as its component. This means that we use our Stack Navigator when we are on the first tab. The second screen gets a newly created <User /> component. You can switch between these tabs with the tab bar, which is created automatically by React Navigation.

Note

You should always install an icon library such as react-native-vector-icons (https://github.com/oblador/react-native-vector-icons) when working with tabs. Such libraries make it easy to find and use expressive icons for your tab bar.

This example, which contains two different navigators, shows the flexibility of React Navigation. We can either use our views in our <Navigator.Screen /> components or use other navigators. This navigator nesting gives us nearly endless possibilities. Please note that in this case, we must hide the header for the first tab because it has already been created by our Stack Navigator. We can do this with the headerShown: false option.

As you can see, navigating with React Navigation is easy and powerful. It also has excellent TypeScript support, as you can see in the repository. You can create types for every layer stack and define exactly what can be passed to the different screens. This includes not only type checking, but also autocomplete functionality in most modern IDEs. You can read more about TypeScript support for React Navigation here: https://reactnavigation.org/docs/typescript/.

React Navigation supports a lot more features, including deeplinking, testing, persisting the navigation state, and integrating different state management solutions. If you want to learn more, please visit the official documentation: https://reactnavigation.org/docs/getting-started/.

Summary

Now that we’ve added a modern navigation library to our example project, it’s time to wrap up this chapter. First, you learned what you have to consider when you wish to style your application. You also learned about the most common solutions for styling React Native applications and learned which of them are suitable for sharing code with web projects.

Then, you learned how to store data locally in a React Native app. Finally, you learned how navigation is different between the web and mobile and how to use a modern navigation library to implement state-of-the-art navigation solutions in React Native apps.

In the next chapter, we’ll look at solutions for creating and maintaining a global app state and how to fetch data from external resources. While learning about this, we’ll fill the placeholder screen we created in this chapter with some cool functionality.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.174.44