Contents

SaaS App Side-Project

Well, while this blog was created to serve my long-term goal of getting a few security certifies under my belt and really going deep on my passion for security - I’m easily distracted. At one point my job was all-consuming, however these days it’s more of a 9-5 gig. I’m still working on incredibly exciting projects and it’s certainly challenging enough to hold my interest, I’m less invested now to work 247 on those problems.

So, on my own time I’m going to start digging into a few new projects. I’ve got some ideas I want to execute on, but I want to take a more simple idea that’s probably been done before and use it as an excuse to learn a new way of developing a product. In the past, I’ve always developed Native Apps (For Mobile) and Web Apps (For Desktop) separately. I want to give React Native a shot, which sounds like a great way to write the code once and deploy it anywhere.

I’m going to make a rather simple Book Club app as a way of learning the framework. I don’t even read books myself, but I’ve got various groups of friends involved in book clubs and hearing them complain about some of the ways they organize themselves it seems like a problem I could easily solve with a simple webapp fairly quickly!

Setup

First, I’ve got to get an environment setup & a skeleton project started… So I installed:
- NodeJS
- Android Studio (I’m on Windows, Not sure how to build for iOS yet)

With that, apparently we want to use a react-native CLI to setup and manage our project:

$ npx react-native
... Installs some stuff ...
... Usage Manpage ...
$ npx react-native init bookclub

               ######                ######
             ###     ####        ####     ###
            ##          ###    ###          ##
            ##             ####             ##
            ##             ####             ##
            ##           ##    ##           ##
            ##         ###      ###         ##
             ##  ########################  ##
          ######    ###            ###    ######
      ###     ##    ##              ##    ##     ###      
   ###         ## ###      ####      ### ##         ###   
  ##           ####      ########      ####           ##  
 ##             ###     ##########     ###             ## 
  ##           ####      ########      ####           ##  
   ###         ## ###      ####      ### ##         ###   
      ###     ##    ##              ##    ##     ###      
          ######    ###            ###    ######
             ##  ########################  ##
            ##         ###      ###         ##
            ##           ##    ##           ##
            ##             ####             ##
            ##             ####             ##
            ##          ###    ###          ##
             ###     ####        ####     ###
               ######                ######


                  Welcome to React Native!
                 Learn once, write anywhere

✔ Downloading template
✔ Copying template
✔ Processing template
✔ Installing dependencies

  Run instructions for iOS:
    • cd "C:\Users\matth\projects\bookclub\bookclub" && npx react-native run-ios
    - or -
    • Open bookclub\ios\bookclub.xcodeproj in Xcode or run "xed -b ios"
    • Hit the Run button

  Run instructions for Android:
    • Have an Android emulator running (quickest way to get started), or a device connected.
    • cd "C:\Users\matth\projects\bookclub\bookclub" && npx react-native run-android

  Run instructions for Windows and macOS:
    • See https://aka.ms/ReactNative for the latest up-to-date instructions.

Okay cool, now we’ve got a project tree to work from:

Directory Tree


Poking around a bit, it looks like this actually generated a sample app for us, so let’s try to load it up and see what it looks like in a browser:

$ npm start
To reload the app press "r"
To open developer menu press "d"

warn No apps connected. Sending "devMenu" to all React Native apps failed. Make sure your app is running in the simulator or on a phone connected via USB.
info Opening developer menu...

Wait, it doesn’t expose a port without a device attached… I did a bit of research, and wow derp - React Native is, well, Native devices only (only mobile)! That’s a bummer this framework doesn’t really behave like I wanted it to.

Guess I’m back looking for a way to have one code-base easily deliver to Web/Mobile…

It seems like what I was actually after was a combination beween React-Native and React-Native-Web!

Let’s run a demo for that then and poke around a bit…

Wipe and try again:

$ rm -r bookclub
$ npm  install --global expo-cli
$ expo init bookclub && cd bookclub
$ expo stat

You’re presented with some kind of little dev portal? This seems pretty nice!
Dev Portal

I Click the run webapp button, and sure enough it loads up in my browser:
Dev Portal

I tried to test out Android/IOS but I didn’t have a simulator running - I’m sure it works fine (I started setting up an Android emulator for later).

Dissecting The Sample

I’ve worked with React/Typescript before, so It shouldn’t take me too long to understand what’s going on here.

Reading through the config files:

{
  "expo": {
    "name": "bookclub",
    "slug": "bookclub",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/images/icon.png",
    "scheme": "myapp",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/images/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true
    },
    "web": {
      "favicon": "./assets/images/favicon.png"
    }
  }
}

Nothing interesting in there…

{
  "main": "node_modules/expo/AppEntry.js",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "eject": "expo eject",
    "test": "jest --watchAll"
  },
  "jest": {
    "preset": "jest-expo"
  },
  "dependencies": {
    "@expo/vector-icons": "^10.0.0",
    "@react-native-community/masked-view": "0.1.10",
    "@react-navigation/bottom-tabs": "^5.8.0",
    "@react-navigation/native": "^5.7.3",
    "@react-navigation/stack": "^5.9.0",
    "expo": "~39.0.2",
    "expo-asset": "~8.2.0",
    "expo-constants": "~9.2.0",
    "expo-font": "~8.3.0",
    "expo-linking": "^1.0.1",
    "expo-splash-screen": "~0.6.1",
    "expo-status-bar": "~1.0.2",
    "expo-web-browser": "~8.5.0",
    "react": "16.13.1",
    "react-dom": "16.13.1",
    "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.2.tar.gz",
    "react-native-gesture-handler": "~1.7.0",
    "react-native-safe-area-context": "3.1.4",
    "react-native-screens": "~2.10.1",
    "react-native-web": "~0.13.12"
  },
  "devDependencies": {
    "@babel/core": "~7.9.0",
    "@types/react": "~16.9.35",
    "@types/react-native": "~0.63.2",
    "jest-expo": "~39.0.0",
    "typescript": "~3.9.5"
  },
  "private": true
}

Alright so, expo is actually the framework we’re using now - it seems to just use react-native + react-native-web under the hood and builds on those? We also get Jest dumped in here in-case I actually care enough to write tests for this (haha not likely).

Other than that, seems pretty simple! No bloat going on that I can see, nice!

Now getting into the ‘meat’, there’s a file called types.tsx:

export type RootStackParamList = {
  Root: undefined;
  NotFound: undefined;
};

export type BottomTabParamList = {
  TabOne: undefined;
  TabTwo: undefined;
};

export type TabOneParamList = {
  TabOneScreen: undefined;
};

export type TabTwoParamList = {
  TabTwoScreen: undefined;
};

This seems to be some sort of state declaration maybe? Not sure yet! Let’s look at App.tsx:

import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import useCachedResources from './hooks/useCachedResources';
import useColorScheme from './hooks/useColorScheme';
import Navigation from './navigation';

export default function App() {
  const isLoadingComplete = useCachedResources();
  const colorScheme = useColorScheme();

  if (!isLoadingComplete) {
    return null;
  } else {
    return (
      <SafeAreaProvider>
        <Navigation colorScheme={colorScheme} />
        <StatusBar />
      </SafeAreaProvider>
    );
  }
}

What’s useCachedResources doing? In ./hooks/useCachedResources we’ve got the source for it:

import { Ionicons } from '@expo/vector-icons';
import * as Font from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import * as React from 'react';

export default function useCachedResources() {
  const [isLoadingComplete, setLoadingComplete] = React.useState(false);

  // Load any resources or data that we need prior to rendering the app
  React.useEffect(() => {
    async function loadResourcesAndDataAsync() {
      try {
        SplashScreen.preventAutoHideAsync();

        // Load fonts
        await Font.loadAsync({
          ...Ionicons.font,
          'space-mono': require('../assets/fonts/SpaceMono-Regular.ttf'),
        });
      } catch (e) {
        // We might want to provide this error information to an error reporting service
        console.warn(e);
      } finally {
        setLoadingComplete(true);
        SplashScreen.hideAsync();
      }
    }

    loadResourcesAndDataAsync();
  }, []);

  return isLoadingComplete;
}

Alright so basically, we’re loading up any resources we’re going to need to render the page and back in Main we’re not going to start rendering anything until this is done, makes sense. I may need to brush up on what exactly the React state/store is and the affect of React.useEffect. Will do that later, let’s continue exploring.

The color scheme piece basically gives us a method of changing the color scheme per mobile versus the web? Perhaps this is just an example of how to provide different hooks for each platform if you need to.

Finally, once loading in complete, we render the component:

 return (
      <SafeAreaProvider>
        <Navigation colorScheme={colorScheme} />
        <StatusBar />
      </SafeAreaProvider>
    );

Now, what’s SafeAreaProvider?
import { SafeAreaProvider } from ‘react-native-safe-area-context’;

“The SafeAreaProvider component is a View from where insets provided by Consumers are relative to. This means that if this view overlaps with any system elements (status bar, notches, etc.) these values will be provided to descendent consumers. Usually you will have one provider at the top of your app.”

Alright that makes sense, nice to have for sure!

Then I suppose the other two elements there are what we’re actually rendering!

Navigation:
import Navigation from ‘./navigation’;
navigation/index.tsx:

import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import * as React from 'react';
import { ColorSchemeName } from 'react-native';

import NotFoundScreen from '../screens/NotFoundScreen';
import { RootStackParamList } from '../types';
import BottomTabNavigator from './BottomTabNavigator';
import LinkingConfiguration from './LinkingConfiguration';

// If you are not familiar with React Navigation, we recommend going through the
// "Fundamentals" guide: https://reactnavigation.org/docs/getting-started
export default function Navigation({ colorScheme }: { colorScheme: ColorSchemeName }) {
  return (
    <NavigationContainer
      linking={LinkingConfiguration}
      theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      <RootNavigator />
    </NavigationContainer>
  );
}

// A root stack navigator is often used for displaying modals on top of all other content
// Read more here: https://reactnavigation.org/docs/modal
const Stack = createStackNavigator<RootStackParamList>();

function RootNavigator() {
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      <Stack.Screen name="Root" component={BottomTabNavigator} />
      <Stack.Screen name="NotFound" component={NotFoundScreen} options={{ title: 'Oops!' }} />
    </Stack.Navigator>
  );
}

LinkingConfiguration.ts

import * as Linking from 'expo-linking';

export default {
  prefixes: [Linking.makeUrl('/')],
  config: {
    screens: {
      Root: {
        screens: {
          TabOne: {
            screens: {
              TabOneScreen: 'one',
            },
          },
          TabTwo: {
            screens: {
              TabTwoScreen: 'two',
            },
          },
        },
      },
      NotFound: '*',
    },
  },
};

BottomTabNavigator.tsx

import { Ionicons } from '@expo/vector-icons';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createStackNavigator } from '@react-navigation/stack';
import * as React from 'react';

import Colors from '../constants/Colors';
import useColorScheme from '../hooks/useColorScheme';
import TabOneScreen from '../screens/TabOneScreen';
import TabTwoScreen from '../screens/TabTwoScreen';
import { BottomTabParamList, TabOneParamList, TabTwoParamList } from '../types';

const BottomTab = createBottomTabNavigator<BottomTabParamList>();

export default function BottomTabNavigator() {
  const colorScheme = useColorScheme();

  return (
    <BottomTab.Navigator
      initialRouteName="TabOne"
      tabBarOptions={{ activeTintColor: Colors[colorScheme].tint }}>
      <BottomTab.Screen
        name="TabOne"
        component={TabOneNavigator}
        options={{
          tabBarIcon: ({ color }) => <TabBarIcon name="ios-code" color={color} />,
        }}
      />
      <BottomTab.Screen
        name="TabTwo"
        component={TabTwoNavigator}
        options={{
          tabBarIcon: ({ color }) => <TabBarIcon name="ios-code" color={color} />,
        }}
      />
    </BottomTab.Navigator>
  );
}

// You can explore the built-in icon families and icons on the web at:
// https://icons.expo.fyi/
function TabBarIcon(props: { name: string; color: string }) {
  return <Ionicons size={30} style={{ marginBottom: -3 }} {...props} />;
}

// Each tab has its own navigation stack, you can read more about this pattern here:
// https://reactnavigation.org/docs/tab-based-navigation#a-stack-navigator-for-each-tab
const TabOneStack = createStackNavigator<TabOneParamList>();

function TabOneNavigator() {
  return (
    <TabOneStack.Navigator>
      <TabOneStack.Screen
        name="TabOneScreen"
        component={TabOneScreen}
        options={{ headerTitle: 'Tab One Title' }}
      />
    </TabOneStack.Navigator>
  );
}

const TabTwoStack = createStackNavigator<TabTwoParamList>();

function TabTwoNavigator() {
  return (
    <TabTwoStack.Navigator>
      <TabTwoStack.Screen
        name="TabTwoScreen"
        component={TabTwoScreen}
        options={{ headerTitle: 'Tab Two Title' }}
      />
    </TabTwoStack.Navigator>
  );
}

Well I think it’s pretty clear how all that comes together! Call me old fashioned, but I personally don’t like a bottom-nav on a webapp. It’s great on mobile though - so eventually I’ll want top-nav on web and bottom nav on mobile. Worry about that later!

The final piece of the puzzle (At a level that I care about for now) is the actual page content!
As we saw in the nav linking, We have three screens, and each one has a typescript file with currently basically no content. Here’s TabOneScreen for example:

import * as React from 'react';
import { StyleSheet } from 'react-native';

import EditScreenInfo from '../components/EditScreenInfo';
import { Text, View } from '../components/Themed';

export default function TabOneScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Tab One</Text>
      <View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
      <EditScreenInfo path="/screens/TabOneScreen.js" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
  },
  separator: {
    marginVertical: 30,
    height: 1,
    width: '80%',
  },
});

Conclusion & Next Steps

Well thats was quick and easy! Usually when doing these sorts of project I would take a quick aside to figuring out how to deploy the app to ensure that process is fully working before investing further into a frameowork - but I’ve deployed react apps to the web before, and the whole point of this is to deploy to mobile so I’m sure I’ll able to overcome that when the time comes!

So, for my next steps I’m going to throw together a bit of a feature set and some primitive wire-framing to work from! From that, I’ll figure out what sort of requirements this bad boy is going to need (Do I need a dedicated backend?) and then finally I’ll slap together a high-level roadmap and get to work!

Directory
$ cd content && tree