Skip to content

Technology Stack

Jake edited this page Jul 16, 2024 · 19 revisions

This section covers the technology used in the Notecards user interface and briefly shows how they are used

React Native - 71 MB

  • JavaScript library for building user interfaces
  • React Native code can be deployed on both iOS and Android mobile platforms
  • Provides platform agnostic native components like View, Text, and Image that map directly to the platform's native UI building blocks
  • Created and maintained by Meta. React Native is supported by a large community resulting in an abundance of resources, libraries, and tools available to help with development

React - 318 kB

  • The React library is used to manage state, both locally (component level) and globally (application level), throughout the application
  • React applications are component-based which promotes breaking the user interface down into reusable components

Global State

  • The global state is provided via React's createContext, useContext and useReducer functions.
  • The createContext object is used with the useReducer function to build a Provider component that wraps the entire application. The resulting Provider component gives all sub components access to global state variables through the use of a custom hook built with React's useContext function. Global state can now be shared between components without passing props manually.
// GlobalState.tsx
import { createContext, useContext, useReducer } from 'react'

const AppStateContext = createContext<AppStateContextTypes | undefined>(undefined);

// Provider component
export const AppStateProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const [state, dispatch] = useReducer(appStateReducer, initialState);

  return (
    <AppStateContext.Provider value={{ state, dispatch }}>
      {children}
    </AppStateContext.Provider>
  );
};

// Custom hook to access the state and dispatch
export const useAppState = () => {
  const context = useContext(AppStateContext);
  if (context === undefined) {
    throw new Error('useAppState must be used within an AppStateProvider');
  }
  return context;
};

// App.tsx
import { AppStateProvider } from './src/context/GlobalState';

function App() {
  return (
    <SafeAreaProvider>
      <ThemeProvider theme={theme}>
        <AppStateProvider>
          <RootNavigator />
        </AppStateProvider>
      </ThemeProvider >
    </SafeAreaProvider>
  )
}
  • Global state within the Notecards application is used to store the logged in user, create new notecards and to display a set of notecards
  • An example of how to retrieve the logged in user via global state can be seen on the Home page:
import { useAppState } from '../context/GlobalState';
// ...

function Home( { navigation }: HomeProps) {

  // Retrieve global state
  const { state } = useAppState();

  useEffect(() => {
    // Retrieve the logged in user
    NotecardService.getNotecardSets(state.user)
      .then((response: AxiosResponse) => {
        // ...
      })
      .catch((error) => {
        // ...
      })
  }, [])

  // ...

  return (
    <View>
      {/* ... */}
    </View>
  )
}

export default Home;

Component level State

  • Local state is scoped to the component that created it along with the sub components that receive it via component props
  • It is created and updated through the use of React's useState function
  • In the below example, the nextButtonLoading boolean is initialized with a value of false. It's value can be updated via the setNextButtonLoading function as seen in the addCard function.
import { useState } from 'react';

const [nextButtonLoading, setNextButtonLoading] = useState(false)

const addCard = () => {
  setNextButtonLoading(true)
  // ... Do work
  setNextButtonLoading(false)
}

return (
  <View>
    ...
    <PrimaryButton
      title={t('nextNotecard')}
      onPressFunction={addCard}
      loading={nextButtonLoading} >
  </View>
)

Components

  • The Notecards application uses functional components. The functional components come in two forms throughout the application. One type is provided to React Navigation screen components to render a screen. The following shows how the details page is structured:
// /views/details.tsx
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { StackParamList } from '../types/DataTypes'

type DetailsProps = NativeStackScreenProps<StackParamList, 'Details'>;

// navigation and route prop is passed in to every screen component
function Details( { navigation, route }: DetailsProps) {
  const { theme } = useTheme();
  const { state, dispatch } = useAppState();
  const { t } = useTranslation();
  const [isLoading, setIsLoading] = useState(true)
  const [confirmationVisibility, setConfirmationVisibility] = useState(false)

  const notecardSet = state.currentNotecardSet

  useEffect(() => {
    // ... Actions to preform when the component is mounted
  }, [navigation])

  const startNotecard = (notecardSet: NotecardSet) => {
    navigation.navigate('Notecard', {name: `${notecardSet.title} ${t('notecardSet')}`, cardId: notecardSet.id})
  }

  if (isLoading) {
    return <Loading />
  }

  const deleteNotecard = () => {
    // ...     
  };

  return (
    <View style={[styles.container, {backgroundColor: theme.colors.secondaryBackground}]}>
      <PrimaryButton
        title={t('start')}
        onPressFunction={() => startNotecard(notecardSet)} >
      </PrimaryButton>
      <Toast />
      <ConfirmationDialog
        toggleDialog={() => setConfirmationVisibility(false)}
        continue={() => deleteNotecard()}
        confirmationTitle={t('confirmation')}
        confirmationText={t('confirmationDeleteNotecardSet')}
        isVisible={confirmationVisibility}
        />
    </View> 
  )
}
  • The screen components use a generic type called NativeStackScreenProps in order to define types for the navigation and route props. The navigation prop enables navigation throughout the navigation stack. In the above example, the navigation prop is used in the startNotecard function to navigate from the details page to the notecards page.
navigation.navigate('Notecard', {name: `${notecardSet.title} ${t('notecardSet')}`, cardId: notecardSet.id})
  • The first parameter of the navigate function is the desired route destination (a string)
  • The second (optional) parameter is an object comprised of the params to pass to the destination route. The example provided passes the Notecard screen component name and cardId params.
  • Within the Notecard screen component, these params can be referenced through the route prop.
// /views/notecard.tsx
const notecardId = route.params.cardId

The format of a non screen component, found within the /components folder, can be seen below:

// /components/primaryButton.tsx
import React, { ReactNode } from 'react';
import { View, Text } from 'react-native';
import {
  Button,
  useTheme
} from '@rneui/themed';
import GlobalStyles from '../styles/GlobalStyles';

const styles = GlobalStyles;

type PrimaryButtonProps = {
  title?: string;
  children?: React.ReactNode | undefined;
  onPressFunction: () => void;
  loading?: boolean;
};

const PrimaryButton: React.FunctionComponent<PrimaryButtonProps> = (props) => {
  const { theme } = useTheme();
  
  return (
    <Button 
      containerStyle={styles.button}
      onPress={props.onPressFunction}
      color={theme.colors.primaryButton}
      loading={props.loading}
      raised={true}
      >
      {props.title ?
        <Text style={[styles.buttonText, {color: theme.colors.primaryText}]}>{props.title}</Text>
      :
        <View>
          {props.children}
        </View>
      }
    </Button>
  )
}

export default PrimaryButton;
  • The above shows a custom button component used throughout the application. It is a functional component that requires a props argument and returns a JSX element. The props the PrimaryButton component accepts can be found in the PrimaryButtonProps type. There, one can see that it accepts three optional props and one required prop. The required property is an on click function that is executed when the button is pressed. The three optional props are title, children, and loading.
  • This component is helpful because it will provide consistent styling and cleaner code. The styling of the button, specifically it's size, background color, font size and font color, are all built into the component, therefore all PrimaryButton's will look the same. The PrimaryButton component is helpful in it's ability to provide cleaner code. It is much cleaner to call <PrimaryButton title={t('start')} onPressFunction={() => startNotecard(notecardSet)} > </PrimaryButton> rather than the code block found in the above return statement each time the button is needed

Component lifecycle hooks

  • React provides the useEffect function that is primarily used in this application to execute code when a component first mounts
import { useEffect } from 'react';
function Home( { navigation }: HomeProps) {
  // ...
  useEffect(() => {
    NotecardService.getNotecardSets(state.user)
      .then((response: AxiosResponse) => {
        // ...
      })
      .catch((error) => {
        // ...
      })
  }, []) // Empty dependency array means this code runs once when the Home component mounts
  // ...
  return (
    <View>
      {/* ... */}
    </View>
  )
}
  • Above, the Home component uses React's useEffect hook to make a request to the server for all the Notecard Sets belonging to the logged in User. This function will be executed once, when the Home component is first mounted
  • In order to execute an "unmount" function, the react useRef hook is used alongside useEffect. An example of how to execute an unmount function in a functional component can be found on the Notecards page:
// /views/notecard.tsx
import { useState, useEffect, useRef } from 'react';

function Notecard( {navigation, route }: NotecardProps) {
  const [viewCount, setViewCount] = useState(1)
  const viewCountRef = useRef(viewCount)
  // ...


  const updateInfluxDbViewCount = (viewCount: number) => {
    // ...
  }

  // Update viewCountRef.current each time viewCount changes
  useEffect(() => {
    viewCountRef.current = viewCount
  }, [viewCount])

  useEffect(() => {
    // Unmount Function
    return () => {
      updateInfluxDbViewCount(viewCountRef.current)
    }
  }, [])

  return (
    {/*...*/}
  )
}

export default Notecard;
  • In the example above, the updateInfluxDbViewCount function is called from inside the return statement of a useEffect hook that has an empty dependency array. This results in updateInfluxDbViewCount being executed once, when a user navigates away from the Notecards page. The value passed to updateInfluxDbViewCount, viewCountRef.current, holds the latest value of the viewCount integer (used to track the amount of notecards the user viewed). The code within the useEffect hook that receives viewCount in it's dependency array is executed each time the value of viewCount changes, subsequently updating the value of viewCountRef.current. This assures that when the updateInfluxDbViewCount function executes, the integer it takes as an argument is accurate. If instead of using viewCountRef, the viewCount value was used as the argument to the updateInfluxDbViewCount function, the value of viewCount would always be one because that was it's value when the useEffect hook was initialized.
  • Used to create universal native apps with React that run on Android, iOS, and the web
  • Integrating the Expo ecosystem with a React Native application makes running local code on different mobile platforms easy
  • Expo is the recommended framework to build React Native apps by React Native

Use

  • Download the Expo CLI on your development environment
  • Download Expo Go on the desired client environment (Android, iOS)
  • Running npx expo start starts the development server
  • Open the Expo Go app on your device and navigate to the specified endpoint where you'll find your application running
  • React navigation provides the functionality for routing throughout the application
  • I made the mistake of not integrating authentication into the application while configuring the React Navigation layout. This resulted in some work arounds involving React Navigation that possibly could have been avoided
  • The React Navigation documentation suggests using a work flow as follows:
isSignedIn ? (
  <>
    <Stack.Screen name="Home" component={HomeScreen} />
    <Stack.Screen name="Profile" component={ProfileScreen} />
    <Stack.Screen name="Settings" component={SettingsScreen} />
  </>
) : (
  <>
    <Stack.Screen name="SignIn" component={SignInScreen} />
    <Stack.Screen name="SignUp" component={SignUpScreen} />
  </>
);
  • Above, different screens are defined based on the isSignedIn condition. Using this work flow did not easily work for me and I opted for an easy and ugly work around that can be seen in /navigation/RootNavigator.tsx. Not properly configuring authentication into the React Navigation and global state relationship was a mistake that I learned from
  • React Navigation provides different types of navigators used to define and manage the navigation structure. The primary types of navigators are: stack, tab, and drawer. With the stack navigator, each new screen is placed on top of the stack. The drawer navigator renders a navigation drawer on the side of the screen which can be opened and closed. The tab navigator lets you switch between different routes via tabs.
  • The Notecards application uses both the stack navigator and as well as a drawer navigator that is nested inside a stack navigator

Drawer Navigator:

  • The header bar throughout the application is configured via React Navigation's stack component
<Stack.Screen
    name="Details"
    component={Details}
    options={ ({ route }) => ({ 
        title: route.params.name,
        headerRight: () => (
            <DeleteButton />
        )
    }) } />
  • Cross platform React Native UI toolkit
  • Used within the Notecard application to provide styling consistency
  • Some of the React Native Elements used: Button, Card, Dialog, Divider, Floating Action Button, Icon, Input, List Item, Switch, Search Bar
  • The main selling point of this library for me was it's ThemeProvider which enables large scale theming. React Native Elements provides an easy way to implement light and dark themes. As seen in App.tsx:
import { createTheme, ThemeProvider } from '@rneui/themed';
const theme = createTheme({
  lightColors: {
    primary: '#e7e7e8',
    primaryBackground: '#faebd7',
    secondaryBackground: '#ffcd94',
    tertiaryBackground: '#65737e',
    error: '#f0360e',
    primaryText: '#3b444b',
    secondaryText: '#946b2d',
    primaryButton: '#f7a583',
    icon: '#3b444b'
  },
  darkColors: {
    primary: '#000',
    primaryBackground: '#3b444b',
    secondaryBackground: '#36454f',
    tertiaryBackground: '#c0c5ce',
    error: '#be2a0a',
    primaryText: '#faebd7',
    secondaryText: '#65737e',
    primaryButton: '#946b2d',
    icon: '#faebd7'
  },
  mode: 'dark',
});

function App() {
  return (
    <SafeAreaProvider>
      <ThemeProvider theme={theme}>
        <AppStateProvider>
          <RootNavigator />
        </AppStateProvider>
      </ThemeProvider >
    </SafeAreaProvider>
  )
}
  • The colors for each "theme set" are defined within a "theme object" that is passed to the ThemeProvider component. The ThemeProvider component then wraps the entire app. This provides all sub components access to the desired color scheme as seen below:
import useTheme from '@rneui/themed';
const { theme } = useTheme();
return (
  <View style={[styles.container, {backgroundColor: theme.colors.secondaryBackground}]}>
    <Text style={{color: theme.colors.primaryText}}>{newTitle}</Text>
  </View>
)
  • Updating the theme can be seen in the CustomDrawerContent, found within the drawer navigator:
// /navigation/CustomDrawerContent.tsx

import { useTheme } from '@rneui/themed';
// ...

function CustomDrawerContent({ navigation, ...props }: CustomDrawerProps ) {
  const { theme, updateTheme } = useTheme();

  const changeTheme = () => {
    const newTheme = theme.mode === 'dark' ? 'light' : 'dark';
    updateTheme({
      mode: newTheme,
    })
    // ...
  }

  return (
    <DrawerContentScrollView {...props}>
      {/* ... */}
        <View style={styles.drawerSwitch}>
          <Switch
            value={theme.mode === 'dark'}
            onValueChange={changeTheme}
          />
        </View>
      {/* ... */
    </DrawerContentScrollView>
  );
}

export default CustomDrawerContent;

'Light' mode of the Drawer Navigator seen above:

i18next - 635 kB

  • i18next is an internationalization-framework written in and for JavaScript
  • The Notecards application can be used in English or Swedish thanks to the i18next framework
  • English and Swedish strings are defined in their respective translation.json files located within the i18n directory

/i18n/sv/translation.json:

{
  "emailInvalid": "Ogiltig e-postadress",
  "filterByTitle": "Filtrera efter titel",
  "front": "Främre"
}

/i18n/en/translation.json:

{
  "emailInvalid": "Invalid Email",
  "filterByTitle": "Filter By Title",
  "front": "Front"
}

See the Home screen component below for how the strings are referenced within the filter bar:

// src/views/home.tsx
import { useTranslation } from 'react-i18next';
// ...

function Home( { navigation }: HomeProps) {
  const { t } = useTranslation();

  // ...

  return (
    <View style={styles.container}>
    {/*...*/}
        <SearchBar
          value={filterString}
          onChangeText={(value) => filterNotecards(value)}
          placeholder={t('filterByTitle')}
          containerStyle={{
            backgroundColor:theme.colors.primaryBackground,
            borderColor:theme.colors.primaryBackground,
            elevation: 10}}
        />
    {/*...*/}
    </View>
  )
}

export default Home;
  • Above, the placeholder param of the SearchBar component accepts t('filterByTitle') as an argument. Depending on which language is currently active, i18n will return the corresponding string assigned to 'filterByTitle' when the component is rendered
  • Changing the application's language can be seen in the Settings component:
// src/views/settings.tsx
import { useTranslation } from 'react-i18next';
// ...

function Settings ({ navigation, route }: SettingProps) {
  const {t, i18n} = useTranslation();

  const changeLanguage = () => {
    const newLanguage = i18n.language === 'sv' ? 'en' : 'sv'
    i18n.changeLanguage(newLanguage)
    save('language', newLanguage)
  }
  // ...

  return (
    <View>
      {/*...*/}
      <PrimaryButton
        title={t('change')}
        onPressFunction={changeLanguage} >
      </PrimaryButton>
    </View>
  )
}

export default Settings;
  • A declarative cross-platform React Native calendar component for iOS and Andriod
  • Github's activity calendar on a user's profile page was the inspiration behind the implementation of this calendar

  • The React Native Calendars component enables style customization for each day. Creating a marked object comprised of the dates a user viewed a notecard and passing it to the markedDates parameter of the CalendarList component tells the calendar which days have a marked value of true. Now, in the dayComponent parameter, querying a date's marking value enables custom styles to the days that a user viewed notecards

Example marked object:

{
  "2024-06-18": {"marked": true},
  "2024-06-19": {"marked": true},
  "2024-07-02": {"marked": true},
  "2024-07-07": {"marked": true}, 
  "2024-07-12": {"marked": true},
  "2024-07-13": {"marked": true}
}
// /src/views/activity.tsx
import { CalendarList } from 'react-native-calendars';
// ...

type ActivityProps = NativeStackScreenProps<StackParamList, 'Activity'>;

function Activity( { navigation, route }: ActivityProps) {
  const [calendarRange, setCalendarRange] = useState(0)
  const [marked, setMarked] = useState({})
  // ...

  useEffect(() => {
    // ...

    function queryRows() {
      // ...

      const formatNativeCalendarsDateString = (dateString) => {
        const date = new Date(dateString);
        // Extract the year, month, and day
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0'); 
        const day = String(date.getDate()).padStart(2, '0');
        // Format the date as YYYY-MM-DD
        return `${year}-${month}-${day}`;
      }

      // Function used to create the object that is provided to the react-native-calendar
      // that displays the days a User viewed notecards
      const createMarkedObject = (activityArray: Array<NotecardActivityObject>) => {
        interface MarkedObject { 
          [date: string] : { marked: boolean; }
        }
        let markedObject: MarkedObject = {}
        for (let i = 0; i < activityArray.length; i++) {
          let notecardActivityDate = activityArray[i]._time
          markedObject[formatNativeCalendarsDateString(notecardActivityDate)] = {marked: true}
        }
        return markedObject
      }

      const calculateCalendarMonthRange = (firstDate: Date) => {
        const today = new Date()
        const yearsDifference = today.getFullYear() - firstDate.getFullYear();
        const monthsDifference = today.getMonth() - firstDate.getMonth();
        return yearsDifference * 12 + monthsDifference;
      }

      // ...

      queryApi.queryRows(query, {
        // ...
        complete: () => {
          if (influxResponse.length) { 
            setMarked(createMarkedObject(influxResponse))
            setCalendarRange(calculateCalendarMonthRange(new Date(influxResponse[0]._time)))
          }
        },
      })
    }
    queryRows()
  }, []);

  return (
    {/*...*/}
    <View>
        <CalendarList
          futureScrollRange={0}
          pastScrollRange={calendarRange}
          displayLoadingIndicator={influxLoading}
          showScrollIndicator={true}
          markedDates={marked}
          dayComponent={({date, state, marking}) => {
            return (
              <View style={{
                backgroundColor: marking ? 'green' : theme.colors.primaryBackground,
                borderWidth: 5,
                borderRadius: 5,
              }}>
                <Text>{'     '}</Text>
              </View>
            );
          }}
          hideDayNames={true}
          style={{
            marginTop: 20,
            marginBottom: 20,
          }}
          theme={{
            calendarBackground: theme.colors.primaryBackground,
            monthTextColor: theme.colors.primaryText,
            indicatorColor: theme.colors.primaryText,
            textMonthFontSize: 16,
          }}
          horizontal={true}
          calendarWidth={320}
        />
    </View> 
  )
}

export default Activity;
  • react-native-toast-message is a popular library used to display custom Toast messages
  • The Notecards application uses this library to display messages that inform the user of successful or failed transactions

import Toast from 'react-native-toast-message';

Toast.show({
  type: 'success',
  text1: t('notecardCreatedSuccessfully'),
  visibilityTime: 1500
});
  • Calling Toast.show() will display the <Toast/> component

Axios - 2.08 MB

  • Promise based HTTP client for the browser and node.js
  • Axios simplifies making HTTP requests and managing responses
  • Axios provides detailed error messages and response objects. JSON response data is automatically parsed to JavaScript objects. Request errors are automatically handled in the catch code block
// src/views/home.tsx
NotecardService.getNotecardSets(state.user)
  .then((response: AxiosResponse) => {
    setNotecards(response.data.notecardSets)
    setIsLoading(false)
  })
  .catch((error) => {
    console.log(`getNotecardSets error: ${error}`)
    setIsLoading(false)
  })