Yannick Mougin
<Code With Yannick/>

<Code With Yannick/>

How to efficiently set up your react native project

How to efficiently set up your react native project

How to create a scalable react native app - Part I

Yannick Mougin's photo
Yannick Mougin

Published on Nov 29, 2021

13 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

  • What are we going to build?
  • Prerequisites
  • Create an expo project
  • Configuring typescript
  • Project architecture
  • Async storage
  • Design system
  • Prepare navigation
  • Conclusion

Creating a new app is a long and complex process that discourages many developers. Keeping your code simple and maintainable are the keys to give your project a better chance of success. In this series, I will show you my way to implement this through a complete step-by-step example.

Disclaimer: This series is composed of three articles. Each part will focus on a different aspect of app development.

This first article of the series is dedicated to the project configuration. Too many times, developers (I was like that too 🀫), do not take the time to make a good project configuration and start coding too quickly. Experience taught me that it is better to spend time at the beginning setting up a pertinent and scalable configuration. In many ways, this will prevent misconceptions, coding errors, rework and other troubles that can happen along the way.

At the end of this series, you will be able to create your own high-quality app with a solid project configuration, consistent design system, data manipulations, and animations.

If you get stuck, you can find a link to my github repo at the end of this article. You can also leave a comment, I will be happy to answer.

What are we going to build?

If like me, you like to learn by doing this tutorial is for you. Before starting, I suggest you to read it once to understand the main concern and then start coding at the same time.

Prerequisites

This tutorial is beginner-friendly, however, to follow it flawlessly you should already have some react native and expo basics. If not please read the getting started doc for react native and expo first. If you have any questions, leave a comment or send me a message on Twitter.

Create an expo project

Open your favorite terminal and let's get started!

If you haven't already, install expo-cli by running :

npm install --global expo-cli

Then, go ahead and create a new expo project :

expo init react-native-note && cd react-native-note

(you can choose another name if you prefer πŸ˜„)

At the question choose a template choose blank (TypeScript)

Typescript is helpful to prevent development errors and so is a huge step in the right direction to become more scalable and productive.

Congrats your project is created!

Try to run it with expo start to ensure everything is working properly.

Configuring typescript

Open your favorite IDE, in my case (like almost everyone) I will use VS Code.

Mac pro tip: CMD + P allow searching for a file in the current project

At the root level of the project, find a file called tsconfig.json and past the content bellow inside :

{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "allowSyntheticDefaultImports": true, // Allow default imports from modules without default export
    "jsx": "react-native", // Specify JSX code generation
    "skipLibCheck": true,
    "moduleResolution": "node", // Specify module resolution strategy
    "noEmit": true, // not emit outputs
    "target": "esnext", // Specify ECMAScript target version
    "baseUrl": ".",
    // create paths to our futur modules
    "paths": {
      "@components/*": ["./src/components/*"],
      "@navigation/*": ["./src/navigation/*"],
      "@contexts/*": ["./src/contexts/*"],
      "@models/*": ["./src/models/*"],
      "@pages/*": ["./src/pages/*"]
    }
  },
  "include": ["./src/**/*"]
}

Project architecture

By default, you will have this project architecture :

default_architecture.png

From here, react native doesn't impose any architecture for your code, technically you could create your components directly at the root. However, it is a good practice to organize your code by concern to improve readability, reusability and debugging.

Let's create some folders.

VS Code tips: you can create parent and child folders at once by specifying their relative path. For example from our root click new folder and input src/pages this will create both src and pages folders (works also with files).

At the root of our project, create an src folder where will live your app. Inside src, create multiple folders called pages (no need to specify what is inside I guess πŸ˜…), components (for child components), hooks, contexts, navigation, and models (for typescript interfaces).

Async storage

We will use async storage to persist user preferences like the selected theme. Install it with the following command :

expo install @react-native-async-storage/async-storage

Add a file src/models/storage.ts, where we will define our storage keys :

using an enum prevent typos

export enum StorageKeys {
  USE_DARK_THEME = 'useDarkTheme'
}

Design system

Dependencies

Defining your design system is one of the most important steps to give your app consistent and neat user visual effects. To handle it in the best way, we will use styled-components, this CSS-in-JS framework provides a simple and flexible way to create react components with the ability to define style based on component props. Link to documentation

npm i styled-system styled-components && npm i --save-dev @types/styled-system @types/styled-components @types/styled-components-react-native babel-plugin-styled-components babel-plugin-module-resolver

and update your babel.config.js file as follow :

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      'babel-plugin-styled-components',
      [
        'module-resolver',
        {
          root: ['./src'],
          extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],
          alias: {
            '@components': './src/components',
            '@navigation': './src/navigation',
            '@contexts': './src/contexts',
            '@models': './src/models',
            '@pages': './src/pages',
          },
        },
      ],
    ],
  };
};

You may have noticed the aliases, you need to configure them both in typescript (we did it above) and babel. Thanks to it, instead of using absolute imports you can use our aliases. For example import SomeComponent from './src/components/SomeComponent' will be import SomeComponent from '@components/SomeComponent'

Run your app with expo start -c to clear your cache and take our changes into consideration.

Styled components has its own syntax, creating a simple full size centered View with background color would be :

// simple example of styled-components
import styled from 'styled-components/native'

const StyledView = styled.View`
  background-color: ${(props) => props.color};
  justify-content: center;
  align-items: center;
  flex: 1;
`;

// then use it like any other component :
<StyledView color=”red”>{...}</StyledView>

In addition to styled-components, we will take advantage of styled-system. This library will extend styled-components improving our style structure by adding styling props directly to your react component. styled-system allow also to control styles based on a global theme object (scales, colors, layouts...). This will upgrade both our design consistency and our code reusability and then will allow us to iterate quickly. Link to documentation

For reference, the same component as above using styled-system will be :

// simple example of styled-components and styled-system
import styled from 'styled-components/native'
import { 
  compose,
  color,
  ColorProps,
  flex,
  flexbox,
  FlexboxProps,
  FlexProps 
} from 'styled-system'

// get our component typed and enjoy autocompletion
type StyledViewProps = ColorProps & FlexProps & FlexboxProps;

const StyledView = styled.View<StyledViewProps>(
  compose(
    color,
    flex,
    flexBox
  )
);
// then use it like :
<StyledView
  flex={1}
  justifyContent='center'
  alignItems='center'
  backgroundColor='red'
>{...}</StyledView>

Pretty easy to read right?

you can even shorten the syntax: backgroundColor become bg, margin become m... but I wanted to keep it simple to understand.

But wait, the real magic happens when you start using theming!

Theming

Inside src/models, create a file named styled.d.ts. This file will extend the empty DefaultTheme interface provided by styled-components to contain our custom theme :

// src/models/style.d.ts
import 'styled-components';

declare module 'styled-components' {
  export interface DefaultTheme {
    fontSizes: number[];
    space: number[];
    colors: {
      primary: string;
      secondary: string;
      background: string;
      text: string;
      accent: string;
    };
  }
}

Then continue by creating a Theme.ts into your src folder and paste the following content inside :

// src/Theme.tsx
import { DefaultTheme } from 'styled-components/native';

// you can play with the colors here
const lightTheme: DefaultTheme = {
  fontSizes: [12, 14, 16, 20, 24, 32, 36], // could also be defined as em
  space: [0, 4, 8, 16, 32, 64],
  colors: {
    primary: '#1A1B41',
    secondary: '#F4845F',
    background: '#FEFEFF',
    text: 'black',
    accent: '#d9dbda',
  },
};

const darkTheme: DefaultTheme = {
  fontSizes: [12, 14, 16, 20, 24, 32, 36],
  space: [0, 4, 8, 16, 32, 64],
  colors: {
    primary: '#7FC29B',
    secondary: '#334756',
    background: '#272838',
    text: 'white',
    accent: '#d9dbda',
  },
};

enum Theme {
  LIGHT = 'light',
  DARK = 'dark',
}

export { lightTheme, darkTheme, Theme };

As you can see, we use the DefaultTheme interface we defined just before to create two themes with different values (especially for colors). We also create an enum to identify each theme and we export everything to be able to use it elsewhere.

Now that we have our themes, we will make use of the React context API to make them available across all our components. styled-components provide a context to expose the selected theme, however, we would like to extend this to be able to switch between themes.

To do this, we will wrap the ThemeProvider from styled-components. Inside src/contexts add a file called ThemeContext.tsx :

// src/contexts/ThemeContext.tsx
import AsyncStorage from '@react-native-async-storage/async-storage';
import React, { createContext, useEffect, useState } from 'react';
import { Alert, useColorScheme } from 'react-native';
import { ThemeProvider as StyledThemeProvider } from 'styled-components/native';
import { StorageKeys } from '@models/storage';
import { darkTheme, lightTheme } from '../Theme';

interface IThemeContext {
  isDarkTheme: boolean | undefined;
  switchTheme: () => void;
}

// create context with default value
export const ThemeContext = createContext<IThemeContext>({
  isDarkTheme: undefined,
  switchTheme: () => {},
});

const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
  // get from OS user prefered theme
  const colorScheme = useColorScheme();
  const [isDark, setIsDark] = useState<boolean>(false);

  // on component first render => check if we have a theme value in async storage
  // otherwise => take user prefered theme
  useEffect(() => {
    const setUserTheme = async () => {
      try {
        const useDarkMode = await AsyncStorage.getItem(StorageKeys.USE_DARK_THEME);
        let result = colorScheme === 'dark';
        if (useDarkMode !== null) {
          result = JSON.parse(useDarkMode);
        }
        setIsDark(result);
      } catch {
        Alert.alert('An error occured getting your saved theme');
      }
    };
    setUserTheme();
  }, []);

  // update theme & set the new value to async storage
  const switchTheme = async () => {
    setIsDark((prev) => !prev);
    try {
      await AsyncStorage.setItem(
        StorageKeys.USE_DARK_THEME,
        JSON.stringify(!isDark)
      );
    } catch {
      Alert.alert('An error occured while updating theme');
    }
  };

  // wrap `styled-components` provider with our own one
  // according to `isDark` provide the corresponding theme from `Theme.ts`
  // every `children` will have access to `isDarkTheme`, `switchTheme` and `theme`
  return (
    <ThemeContext.Provider
      value={{
        isDarkTheme: isDark,
        switchTheme,
      }}
    >
      <StyledThemeProvider theme={isDark ? darkTheme : lightTheme}>
        {children}
      </StyledThemeProvider>
    </ThemeContext.Provider>
  );
};

export default ThemeProvider;

All you have to do now is to wrap all your app within this context. After that, every components can have access to the current theme through the useTheme hook. Even better, our styled components can directly use theme variables in their props. Update your App.tsx to use it :

color or backgroundColor props can now use any colors from our theme (you can still input color that doesn't exist in theme like #6DD8AF or "red" for example).

margin padding props use values from "space" array in our theme. For example when you set margin={2} the value "2" represents the index in this array. If you go further than the available indexes, the value will be used as pixels.

fontSize use values from "fontSizes" array in our theme. Just like margin and padding it will be interpreted as index in the array. Check the styled-system doc out for more.

// App.tsx
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { SafeAreaView, StyleSheet, Text } from 'react-native';
import styled from 'styled-components/native';
import {
  color,
  ColorProps,
  compose,
  flex,
  flexbox,
  FlexboxProps,
  FlexProps,
} from 'styled-system';
import ThemeProvider from '@contexts/ThemeContext';

type StyledViewProps = ColorProps & FlexProps & FlexboxProps;
const StyledView = styled.View<StyledViewProps>(compose(color, flex, flexbox));

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <ThemeProvider>
        <StyledView
          flex={1}
          justifyContent='center'
          alignItems='center'
          backgroundColor='primary'
        >
          <Text style={styles.title}>Hello from App</Text>
        </StyledView>
        <StatusBar style='auto' />
      </ThemeProvider>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  title: {
    color: 'white',
  },
});

Refresh your app to enjoy your first themed view!

Generic components

To avoid re-declaring the most used components (View and Text) in every page we are going to create generic components to reuse.

In src/components create two components StyledView.tsx and StyledText.tsx :

// src/components/StyledView.tsx
import styled from 'styled-components/native';
import {
  color,
  ColorProps,
  compose,
  flex,
  flexbox,
  FlexboxProps,
  FlexProps,
  layout,
  LayoutProps,
  position,
  PositionProps,
  space,
  SpaceProps,
  borderRadius,
  BorderRadiusProps
} from 'styled-system';

// space allow to use padding, margin...
// layout allow to use width, height...
type StyledViewProps = ColorProps &
  SpaceProps &
  PositionProps &
  FlexProps &
  FlexboxProps &
  LayoutProps &
  BorderRadiusProps;

const StyledView = styled.View<StyledViewProps>(
  compose(color, space, position, flex, flexbox, layout, borderRadius)
);

export default StyledView;
import styled from 'styled-components/native';
import {
  color,
  ColorProps,
  compose,
  space,
  SpaceProps,
  typography,
  TypographyProps,
} from 'styled-system';

// typography allow to change font size, style etc
type StyledTextProps = ColorProps & SpaceProps & TypographyProps;

const StyledText = styled.Text<StyledTextProps>(
  compose(color, space, typography)
);

export default StyledText;

As you can see, we pass common used style properties to our component via the compose helper function. Thanks to it we will now have access to those properties directly through the component props

Ok, so let's use it!

Prepare navigation

For this app, we are going to use a famous library called React Navigation. It allows to easily handle multiple navigation patterns such as tabs, drawers, stacks...

In our case, we are going to use a stack. If you want to learn more about other possibilities, leave a comment and I will consider creating a dedicated article.

Install the library and its dependencies with the following command :

npm install @react-navigation/native @react-navigation/native-stack && expo install react-native-screens react-native-safe-area-context

While installing, create two "placeholder" components respectively named Home.tsx and NoteEdit.tsx inside src/pages folder.

Pro tips: I'm using the ES7 React/Redux/GraphQL/React-Native snippets extension to speed up the creation process. With it, add a new file and write rnf inside to create a functional component.

// src/pages/Home.tsx
import React from 'react';
import StyledText from '@components/StyledText';
import StyledView from '@components/StyledView';

export default function Home() {
  const { switchTheme } = useContext(ThemeContext);

  return (
    <StyledView
      flex={1}
      justifyContent='center'
      alignItems='center'
      backgroundColor='primary'
    >
      <StyledText color='white'>Hello from Home</StyledText>
      <StyledText color='secondary' mt={3} onPress={switchTheme}>
        Click to switch theme
      </StyledText>
    </StyledView>
  );
}
// src/pages/NoteEdit.tsx
import React from 'react';
import StyledText from '@components/StyledText';
import StyledView from '@components/StyledView';

export default function NoteEdit() {
  return (
    <StyledView
      flex={1}
      justifyContent='center'
      alignItems='center'
      backgroundColor='background'
    >
      <StyledText color='text'>Hello from NoteEdit</StyledText>
    </StyledView>
  );
}

With these two components, you get a first glimpse of why a good project setup could improve your productivity. Our components are much more readable and concise.

Once it's done, create a file src/navigation/HomeStack.tsx. In this file, we are going to create our main stack navigator with our two screens.

// src/navigation/HomeStack.tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
import { useTheme } from 'styled-components/native';
import { Note } from '@models/note';
import StyledView from '@components/StyledView';
import Home from '@pages/Home';
import NoteEdit from '@pages/NoteEdit';

// type navigation + each route params
export type HomeStackParamList = {
  Home: undefined;
  NoteEdit: { id: number; title: string };
};

// using type avoid typo in route name by providing autocomplete
const HomeStack = createNativeStackNavigator<HomeStackParamList>();

export default function HomeStackNavigator() {
  const { background, text } = useTheme().colors;
  // by default => go to Home
  // wrapping everything in a view with bg color avoid
  // white flickering in dark mode
  return (
   <StyledView flex={1} backgroundColor='background'>
    <HomeStack.Navigator initialRouteName="Home" >
      <HomeStack.Screen
        name='Home'
        component={Home}
        options={() => ({
          headerShown: false,
        })}
      />
      <HomeStack.Screen
        name='NoteEdit'
        component={NoteEdit}
        options={({ route }) => ({
          headerStyle: { backgroundColor: background },
          headerTintColor: text,
          headerTitle: route.params.title,
        })}
      />
    </HomeStack.Navigator>
   </StyledView>
  );
}

And then add an src/navigation/index.tsx where we will create our main navigation container :

// src/navigation/index.tsx
import { NavigationContainer } from '@react-navigation/native';
import React from 'react';
import HomeStackNavigator from '.@navigation/HomeStack';

export default function Navigation() {
  return (
    <NavigationContainer>
      <HomeStackNavigator />
    </NavigationContainer>
  );
}

Finally, we need to use this component in our App.tsx

import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { SafeAreaView, StyleSheet } from 'react-native';
import ThemeProvider from '@contexts/ThemeContext';
import Navigation from '@navigation/index';
-import {
-  color,
-  ColorProps,
-  compose,
-  flex,
-  flexbox,
-  FlexboxProps,
-  FlexProps,
-} from 'styled-system';

-type StyledViewProps = ColorProps & FlexProps & FlexboxProps;
-const StyledView = styled.View<StyledViewProps>(compose(color, flex, flexbox));

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <ThemeProvider>
-        <StyledView
-          flex={1}
-          justifyContent='center'
-          alignItems='center'
-          backgroundColor='primary'
-        >
-          <Text style={styles.title}>Hello from App</Text>
-        </StyledView>
+       <Navigation />
        <StatusBar style='auto' />
      </ThemeProvider>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
-  title: {
-    color: 'white',
-  },
});

Run your app, you should see your Home page, try to click on the "click to switch theme". Congrats you successfully setup your project!

Your favorite theme is even persisted in your mobile storage πŸ€—

Conclusion

This part wasn't the funniest, but having all of this already configured will really be a game changer for the rest of the tutorial. This will allow us to iterate quickly while keeping our code clean.

In the next part, we will continue by building our pages, components and business logic. We will see among other things how to create a masonry layout and a search feature.

I hope you enjoy and learn something interesting with this tutorial. If you have any suggestion / question feel free to leave a comment, I will be happy to answer it.

Subscribe to my newsletter to get notified when the next article comes out!

Link to my repo

See you soon! πŸ‘‹

- Yannick

Β 
Share this