-
Notifications
You must be signed in to change notification settings - Fork 0
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
, andImage
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
- The global state is provided via React's
createContext
,useContext
anduseReducer
functions. - The
createContext
object is used with theuseReducer
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'suseContext
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;
- 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 thesetNextButtonLoading
function as seen in theaddCard
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>
)
- 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 thenavigation
androute
props. Thenavigation
prop enables navigation throughout the navigation stack. In the above example, thenavigation
prop is used in thestartNotecard
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 componentname
andcardId
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
- 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 alongsideuseEffect
. 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 auseEffect
hook that has an empty dependency array. This results inupdateInfluxDbViewCount
being executed once, when a user navigates away from the Notecards page. The value passed toupdateInfluxDbViewCount
,viewCountRef.current
, holds the latest value of theviewCount
integer (used to track the amount of notecards the user viewed). The code within theuseEffect
hook that receivesviewCount
in it's dependency array is executed each time the value ofviewCount
changes, subsequently updating the value ofviewCountRef.current
. This assures that when theupdateInfluxDbViewCount
function executes, the integer it takes as an argument is accurate. If instead of usingviewCountRef
, theviewCount
value was used as the argument to theupdateInfluxDbViewCount
function, the value ofviewCount
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
- 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 - 311 kB
- 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 />
)
}) } />
React Native Elements - 350 kB
- 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 theSearchBar
component acceptst('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;
React Native Calendars - 423 kB
- 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 themarkedDates
parameter of theCalendarList
component tells the calendar which days have a marked value of true. Now, in thedayComponent
parameter, querying a date'smarking
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 - 42 kB
- 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)
})