1. Expo & Core Components
Tijdens deze les bouwen we een eenvoudige To-Do applicatie waarin taken toegevoegd kunnen worden en als afgewerkt of niet-afgewerkt gemarkeerd kunnen worden.
De startbestanden voor deze les zijn anders opgebouwd dan bij de frontend-lessen. Omwille van de complexiteit van het project is een deel van de lesinhoud al geïmplementeerd omdat er anders te veel bestanden verplaatst moeten worden binnen het project, wat verwarrend kan zijn.
Startbestanden
Waarschuwing
De code in deze lessen is HEEL gevoelig aan lange, geneste padnamen. Als je op Windows machine werkt, is het ten zeerste aangeraden om je projecten in c:\mobile_dev te plaatsen en geen diepere mappenstructuur te gebruiken.
Het compileren van een applicatie kan tot 20 minuten duren (de eerste keer), als je dan minutenlang moet wachten op uiteindelijk een foutmelding te krijgen ben je heel veel tijd verloren. Begin dus op een pad dat zo dicht mogelijk bij de root van je filesystem ligt en vermijd dit soort problemen.
Expo
Doorheen deze lessenreeks maken we gebruik van Expo, een meta-framework voor React Native waarmee we snel een eenvoudig mobiele applicaties kunnen bouwen.
Het is perfect mogelijk om een React Native applicatie te bouwen zonder Expo, maar dit is aanzienlijk complexer en vereist dat je zelf aanpassingen doet aan het Android en iOS project. Daarnaast is het lastiger om routing werkende te krijgen en om native modules (camera, bluetooth, ...) te gebruiken.
Project aanmaken
Bovenstaande startbestanden bevatten reeds een React Native project waarin ESLint en Prettier geïntegreerd zijn. Ondanks dat de startbestanden reeds een project bevatten, is het wel cruciaal dat je zelf een project kan aanmaken, daarom bespreken we procedure hieronder.
Een project aanmaken kan via onderstaand commando. Voor de configuratie van ESLint en Prettier verwijzen we door naar de appendix.
pnpm create expo-app --templatenpx create-expo-app --templatebun create expo-app --templateyarn create expo-app --templateVie de --template parameter kunnen we kiezen uit enkele templates. Deze les kiezen we voor de Blank (TypeScript) template, voor de volgende lessen vertrekken we van de Navigation (TypeScript) template.
? Choose a template: » - Use arrow-keys. Return to submit.
Blank
> Blank (TypeScript) - blank app with TypeScript enabled
Navigation (TypeScript)
Blank (Bare)Vervolgens moeten we de naam van de app ingeven, waarna onderstaande uitvoer verschijnt.
✅ Your project is ready!
To run your project, navigate to the directory and run one of the following pnpm commands.
- cd mobile_dev_lecture1_example
- pnpm run android
- pnpm run ios # you need to use macOS to build the iOS project - use the Expo app if you need to do iOS development without a Mac
- pnpm run webAls we onderstaand commando uitvoeren wordt de app gecompileerd.
pnpm startnpm run startbun startyarn run startVervolgens typen we de letter 'a' in om het project te openen in een Android emulator of 'i' om het project te openen in een iOS emulator. Daarna opent de emulator en wordt Expo Go geïnstalleerd[1] op de emulator. Eens deze installatie gedaan is, wordt Expo Go gebruikt om de applicatie te openen.

Core components
Voordat we de To-Do app uitwerken, kijken we eerste naar de enkele basiscomponenten en hoe we deze een opmaak kunnen geven in React Native. React Native code lijkt natuurlijk heel sterk op de code die we in frontend frameworks geschreven hebben, maar komt niet volledig overeen.
Om dit te illustreren benaderen we applicatie eerst als een normale React applicatie.
export default function App() {
return <div>Een eerste React Native App</div>
}Als we bovenstaande code uitvoeren (via pnpm android), krijgen we onderstaande foutmelding te zien in de console.
De reden voor deze fout is dat je in React Native enkel gebruik kan maken van core components. Door een recente update van Expo kan je hier een uitzondering op maken[2], maar daar gaan we verder niet op in.
Begrip: Core Components
Omdat React Native de UI die in JavaScript geschreven is converteert naar native UI-elementen, is het aantal componenten die door React Native aangeboden worden eerder beperkt. Klassieke HTML tags kunnen niet gebruikt worden.
Elk van de core components is gelinkt aan een vast UI-element in de Android of iOS SDK. Alhoewel de standaard componenten eenvoudig zijn, kunnen deze wel gebruikt worden om complexere en grotere componenten te bouwen.
Het is belangrijk om op te merken dat verschillende cruciale componenten, zoals een checkbox, radio button, ... niet beschikbaar zijn in React Native. Als je deze wilt gebruiken, moet je de component zelf bouwen, of gebruikmaken van een community library.
View & Text Components
De View component is de basis bouwsteen die je voor alle andere componenten moet gebruiken. Het ingangspunt van elke applicatie moet een View zijn.
Deze component is vergelijkbaar met een <div> en kan dan ook, net zoals een <div>, nul, één of meerdere kinderen bevatten. Daarnaast komt ook de styling overeen met een <div>, een View is dus een container zonder opmaak die standaard de volledige breedte van een scherm in beslag neemt.
De Text component dient, zoals de naam doet vermoeden, om tekst weer te geven.
Deze component is vergelijkbaar met een paragraph (<p>) in HTML, er zijn wel enkele belangrijke verschillen. Text componenten kunnen genest worden, terwijl dat voor een <p> tag verboden is. Verder kan een Text component een newline bevatten, de '\n' literal produceert een linebreak in de resulterende app.
Via deze twee componenten kunnen we bovenstaande code omvormen naar iets dat wel door React Native gecompileerd kan worden.
import {Text, View} from 'react-native'
export default function App() {
return (
<View>
<Text>Een eerste React Native App</Text>
</View>
)
}Alhoewel de app nu compileert, zijn er duidelijk nog visuele problemen. De tekst staat onder de statusbalk en is dus niet leesbaar.

SafeAreaView
Een smartphone bevat verschillende elementen die de content van een applicatie kunnen overlappen. Denk hierbij aan een notch, een afgerond scherm, een home button, ... Het is vanzelfsprekend dat de applicatie niet oder deze elementen mag staan, om dit probleem op te lossen kunnen we gebruik maken van de safe-area-context library.
pnpx expo install react-native-safe-area-contextnpx expo install react-native-safe-area-contextbunx expo install react-native-safe-area-contextyarn dlx expo install react-native-safe-area-contextMerk op dat we pnpx expo gebruiken om de library te installeren, dit garandeert dat de versie van react-native-safe-area-context die geïnstalleerd wordt compatibel is met de versie van Expo die we gebruiken.
De library bevat twee componenten, SafeAreaProvider zorgt ervoor dat de SafeAreaView[3] component werkt. Deze laatste component voeg automatisch marge toe aan de randen van de view zodat er geen overlap is met hardware-elementen.
Info
Als onderstaande code geen zichtbaar resultaat geeft, kan je de cache leegmaken met onderstaand commando:
pnpm start --clearnpm run start --clearbun start --clearyarn run start --clearimport {Text} from 'react-native'
import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'
export default function App() {
return (
<SafeAreaProvider>
<SafeAreaView>
<Text>Een eerste React Native App</Text>
</SafeAreaView>
</SafeAreaProvider>
)
}
Theming
De app ziet er nog heel basic uit, het is altijd interessanter als een app een eigen thema heeft. Daarom is er in de startbestanden al een context en bijhorende provider voorzien.
Begrip: Styling in React Native
Om de opmaak van een React Native applicatie te implementeren wordt iets gebruikt dat op CSS lijkt, maar niet helemaal overeen komt. Er zijn enkele belangrijke verschillen tussen CSS en React Native styling.
- Id's, klassen en andere CSS-selectors kunnen niet gebruikt worden
- Lay-out gebeurt uitsluitend met flexbox. Er zijn wel enkele verschillen met CSS-flexbox.
- flexDirection heeft column als default
- alignContent heeft flex-start als default
- flexShrink heeft 0 als default
- flex kan slechts een waarde krijgen, een getal.
- Er zijn twee mogelijke eenheden rem, em, px, ... worden niet gebruikt.
- Als de hoogte en/of breedte zonder eenheid (en als integer) genoteerd wordt, verwijst dit naar density-independent pixels.
- Als de hoogte en/of breedte als '%' genoteerd wordt, verwijst dit naar procentuele afmetingen.
- Een View component moet een vaste hoogte of breedte hebben of moet de regel flex: krijgen waarbij een integer groter dan of gelijk aan is. Als dit niet het geval is, zijn de kinderen niet zichtbaar.
- Er is GEEN overerving van View naar Tekst, dit betekent dat dingen als tekst kleur voor elke Text component individueel aangepast moet worden. Behalve als de Text component in een andere Text component genest is.
Opmaak wordt gedefinieerd via de StyleSheet namespace en wordt volledig in JavaScript genoteerd.
import {View, Text, StyleSheet} from 'react-native'
import {FunctionComponent} from 'react'
import {StatusBar} from 'expo-status-bar'
const Foo: FunctionComponent = () => {
return (
<View style={styles.container}>
<Text style={[styles.textStyle, styles.strikeThrough]}>
Je kan één of meer stijlen toekennen aan een component.
De laatste stijl heeft voorrang.
De kleur van deze tekst is dus rood.
</Text>
<StatusBar style="auto" />
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#282c34',
alignItems: 'center',
justifyContent: 'center',
},
textStyle: {
color: '#fff',
fontSize: 18,
},
strikeThrough: {
color: 'red',
textDecorationLine: 'line-through',
},
})Om de waarde uit de provider uit te lezen, moet deze natuurlijk wel rond de component staan waarin we dit willen uitlezen. Daarom omringen we de TaskPage component uit de startbestanden met de ThemeProvider component.
import TaskPage from './src/pages/tasks/taskPage'
import ThemeProvider from './src/context/themeProvider'
export default function App() {
return (
<ThemeProvider>
<TaskPage />
</ThemeProvider>
)
}const TaskPage: FunctionComponent = () => {
return (
<SafeAreaProvider>
<SafeAreaView>
<Text>Een eerste React Native App</Text>
</SafeAreaView>
</SafeAreaProvider>
)
}Als we de ThemeContext vervolgens gebruiken om de achtergrondkleur en de bijhorende tekstkleur uit te lezen stoten we op een nieuwe problemen.
const TaskPage: FunctionComponent = () => {
const {backgroundColor, textColor} = useContext(ThemeContext)
return (
<SafeAreaProvider>
<SafeAreaView
style={[{backgroundColor, color: textColor}]}>
<Text>Een eerste React Native App</Text>
</SafeAreaView>
</SafeAreaProvider>
)
}Ten eerste geeft TypeScript onderstaande foutmelding:
De color property is niet beschikbaar op een View component. In tegenstelling tot klassieke HTML, kan de tekstkleur dus niet globaal ingesteld worden, maar moet dit gebeuren op het individueel niveau van een Text component. De tekstkleur wordt enkel overgeërfd van een bovenliggende Text component, en een Text component kan enkel andere Text componenten als kind hebben, niet bijzonder handig.
const TaskPage: FunctionComponent = () => {
const {backgroundColor, textColor} = useContext(ThemeContext)
return (
<SafeAreaProvider>
<SafeAreaView style={[{backgroundColor}]}>
<Text style={[{color: textColor}]}>Een eerste React Native App</Text>
</SafeAreaView>
</SafeAreaProvider>
)
}Zelfs na deze aanpassing is de app nog niet ideaal. Zoals onderstaand screenshot demonstreert, wordt de achtergrond slechts op een klein stukje van het scherm toegepast.

Om dit probleem op te lossen moeten we de hoogte van de View component instellen. Hiervoor moeten we gebruik maken van flexbox, de enige manier om een lay-out te bouwen in React Native, zaken zoals CSS-Grid zijn niet beschikbaar[4]. Flexbox werkt op ongeveer dezelfde manier in React Native als in een klassieke website, maar er zijn wel enkele kleine verschillen.
- flexDirection heeft column als default
- alignContent heeft flex-start als default
- flexShrink heeft 0 als default in de plaats van 1
- flex kan slechts een waarde krijgen, een getal
const TaskPage: FunctionComponent = () => {
const {backgroundColor, textColor} = useContext(ThemeContext)
return (
<SafeAreaProvider>
<SafeAreaView style={[{backgroundColor, flex: 1}]}>
<Text style={[{color: textColor}]}>Een eerste React Native App</Text>
</SafeAreaView>
</SafeAreaProvider>
)
}Bovenstaande code produceert nu een veel beter resultaat, als is de statusbalk nog steeds een probleem.

Voordat we naar de statusbalk kijken, is het interessant om eerste de code in de TaskPage te herschrijven. Ook al hebben we maar drie CSS-regels gekoppeld aan de View component, is het nog steeds properder om deze code af te zonderen. We kunnen geen klassieke CSS gebruiken, we moeten de StyleSheet klasse gebruiken. Op deze manier wordt de code afgezonderd en wordt het object met styling niet in elke re-render opnieuw aangemaakt.
De klassen die we definiëren in het stylesheet kunnen meegegeven worden aan de style property van een core-component. We kunnen, via deze property, één stylesheet toekennen, of een array met meerdere stylesheet of objecten. Net zoals in CSS, is het mogelijk om dingen te overschrijven, het laatste element in de array heeft steeds voorrang.
De dynamische delen van de CSS, i.e. de kleuren uit de ThemeProvider, kunnen niet in de styles variable bewaard worden omdat deze gewijzigd kunnen worden door de gebruiker en dus niet statisch zijn, daarom gebruiken we voor deze properties inline styles.
import {Text, StyleSheet} from 'react-native'
const TaskPage: FunctionComponent = () => {
const {backgroundColor, textColor} = useContext(ThemeContext)
return (
<SafeAreaProvider>
<SafeAreaView style={[styles.container, {backgroundColor}]}>
<Text style={[{color: textColor}]}>Een eerste React Native App</Text>
</SafeAreaView>
</SafeAreaProvider>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
})Statusbar
De StatusBar component uit Expo is een uitbreiding van de standaard React Native component met dezelfde naam. De Expo versie biedt enkele verbeteringen die vooral met styling te maken hebben. Een applicatie functioneert perfect zonder dat deze component gebruikt wordt, maar zoals bovenstaande screenshots illustreren, is het interessanter om deze component toch toe te voegen zodat de statusbar mee gestyled wordt met de rest van de applicatie.
Via de style property kunnen we de tekstkleur aanpassen, maar we kunnen enkel kiezen uit de strings light, dark, auto en inverted.
Aangezien de statusbalk zich in de volledige app op eenzelfde manier moet gedragen, voegen we de StatusBar component toe aan de ThemeProvider.
Info
De StatusBar heeft enkele properties zoals backgroundColor, translucent, ... die niet meer werken op de nieuwste Android versies (en nooit gewerkt hebben op iOS).
Vanaf versie 15 forceert Android dat applicaties edge-to-edge gebruiken. Hierdoor wordt de statusbalk (zowel boven als onderaan) een deel van de applicatie en worden deze niet langer boven de rest van de applicatie gerenderd.
Omdat de statusbalk nu een deel van de applicatie is, krijg deze de achtergrond kleur van de applicatie en is het niet langer mogelijk om deze apart te stylen.
import {StatusBar} from 'expo-status-bar'
const ThemeProvider: FunctionComponent<PropsWithChildren> = ({children}) => {
return (
<ThemeContext.Provider value={darkTheme}>
<StatusBar style="auto" />
{children}
</ThemeContext.Provider>
)
}Onderstaand screenshot toont het resultaat van de drie meest frequente keuzes. Deze zijn genomen op een toestel waar het OS-thema op dark staat.

Zoals te zien is op screenshot moeten we de style property op light zetten om een goed resultaat te bekomen, dit is verre van ideaal. De statusbalk zou zich automatisch moeten aanpassen aan de hand van het gekozen OS-thema. Dit is dan ook wat de auto waarde doet, maar voorlopig doet deze waarde niets. Het probleem is dat de app het gekozen theme voorlopig negeert, om dit te veranderen moeten we app.json aanpassen.
{
"expo": {
"name": "mobile_dev_lecture1_example",
"slug": "mobile_dev_lecture1_example",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}Als we de app vervolgens herladen en de style eigenschap op auto zetten, past de statusbar zich automatisch aan.
useColorScheme
De statusbalk past zich nu wel aan, maar het thema van de applicatie nog niet. Hiervoor kunnen we de useColorScheme hook gebruiken.
Begrip: useColorScheme
De useColorScheme hook kan gebruikt worden om het voorkeursthema van de gebruiker uit te lezen.
De hook geeft een string terug die één van onderstaande waarden heeft:
- light: De gebruiker verkiest een licht thema
- dark: De gebruiker verkiest een donker thema
- null: De gebruiker heeft geen voorkeur opgegeven. Normaliter is dit enkel het geval op oudere toestellen waar deze optie nog niet beschikbaar was.
import {useColorScheme} from 'react-native'
const Foo: FunctionComponent = () => {
const colorScheme = useColorScheme()
if (colorScheme === 'light') {
return <>{/* A light styled component */}</>
} else if (colorScheme === 'dark') {
return <>{/* A dark styled component */}</>
} else if (colorsScheme === null) {
return <>{/* A component styled in the app's prefered theme */}</>
}
}Via deze hook kunnen we de ThemeProvider uitbreiden zodat het theme zich automatisch aanpast aan het OS-thema.
import {useColorScheme} from 'react-native'
const ThemeProvider: FunctionComponent<PropsWithChildren> = ({children}) => {
const isDark = useColorScheme() === 'dark'
return (
<ThemeContext.Provider value={isDark ? darkTheme : lightTheme}>
<StatusBar style="auto" />
{children}
</ThemeContext.Provider>
)
}De app past zich nu volledig aan, zoals geïllustreerd in onderstaande video.
StyledText
De startbestanden bevatten reeds een Header component om de titel van de pagina weer te geven en eventueel een knop.
Merk op dat de Header component de achtergrondkleur van de header aanpast aan de hand van de waarde uit de ThemeContext. Omdat dit een andere kleur is dan diegene op de SafeAreaView component, moeten we de achtergrondkleur van de SafeAreaView aanpassen en een nieuwe geneste view toevoegen die de achtergrondkleur van de app instelt.
const Header: FunctionComponent<TitleBarProps> = ({title, children}) => {
const {surfaceColor, textColor} = useContext(ThemeContext)
return (
<View style={[style.container, {backgroundColor: surfaceColor}]}>
<View style={[style.titleContainer]}>
<Text style={[style.title, {color: textColor}]}>{title}</Text>
</View>
<View style={[style.toolbar]}>{children}</View>
</View>
)
}const TaskPage: FunctionComponent = () => {
const {backgroundColor, surfaceColor, textColor} = useContext(ThemeContext)
return (
<SafeAreaProvider>
<SafeAreaView style={[{backgroundColor: surfaceColor}, styles.container]}>
<Header title="Tasks" />
<View style={[styles.container, {backgroundColor}]}>
<Text style={[{color: textColor}]}>Een eerste React Native App</Text>
</View>
</SafeAreaView>
</SafeAreaProvider>
)
}Na bovenstaande aanpassingen ziet de app er als volgt uit.

Bovenstaande code werkt, maar we gebruiken zowel in de Header als in de TaskPage component de ThemeContext om de tekstkleur op te halen en die vervolgens door te geven aan de Text component. Dit wordt snel heel omslachtig, zeker als de applicatie groter wordt. Daarom is het interessanter om een herbruikbare StyledText component te bouwen die de kleur automatisch toepast.
Omdat de StyledText component een wrapper is rond een Text component, is het noodzakelijk dat alle properties van Text doorgegeven kunnen worden aan de StyledText component. Om dit te realiseren, kunnen we gebruik maken van de ComponentProps helper die door React geëxporteerd wordt.[5]
Begrip: ComponentProps
Via de ComponentProps helper is het mogelijk om een interface/type te generen voor een component die de verwachte properties niet exporteert als interface/type.
De helper ook gebruikt worden om de properties van een klassiek HTML-element te weten te komen, maar hiervoor zijn binnen React ook interfaces voor voorzien die je rechtstreeks kunt importeren.
import {ComponentProps} from 'react'
import Foo from 'foo'
type fooProps = ComponentProps<typeof Foo>
type inputProps = ComponentProps<'input'>Alle properties, met uitzondering van style en children, kunnen rechtstreeks doorgegeven worden van de StyledText component naar de onderliggende Text component. Aangezien we de tekstkleur standaard willen instellen in de StyledText component en omdat we deze standaard styling moeten kunnen uitbreiden of aanpassen, moeten we de style property van de StyledText component combineren met de styling voor de tekstkleur. De children property moet natuurlijk doorgegeven worden als kind aan de onderliggende Text component.
We vermelden de children en style properties expliciet en gebruiken vervolgende de spread operator om alle andere properties te bundelen in een nieuw object dat we, opnieuw via de spread operator, doorgeven aan de Text component.
De nieuwe component kan vervolgens gebruikt worden in de Header en TaskPage componenten.
import {ComponentProps, FunctionComponent, useContext} from 'react'
type StyledTextProps = ComponentProps<typeof Text>
const StyledText: FunctionComponent<StyledTextProps> = ({children, style, ...textProps}) => {
const {textColor} = useContext(ThemeContext)
return (
<Text style={[{color: textColor}, style]} {...textProps}>
{children}
</Text>
)
}const Header: FunctionComponent<TitleBarProps> = ({title, children}) => {
const {surfaceColor} = useContext(ThemeContext)
return (
<View style={[style.container, {backgroundColor: surfaceColor}]}>
<View style={[style.titleContainer]}>
<StyledText style={[style.title]}>{title}</StyledText>
</View>
<View style={[style.toolbar]}>{children}</View>
</View>
)
}const TaskPage: FunctionComponent = () => {
const {backgroundColor, surfaceColor} = useContext(ThemeContext)
return (
<SafeAreaProvider>
<SafeAreaView style={[{backgroundColor: surfaceColor}, styles.container]}>
<Header title="Tasks" />
<View style={[styles.container, {backgroundColor}]}>
<StyledText>Een eerste React Native App</StyledText>{' '}
</View>
</SafeAreaView>
</SafeAreaProvider>
)
}Het is duidelijk dat je best altijd gestylede versies van de meest gebruikte core components maakt (tenzij je een componenten library gebruikt).
Buttons
In de header moet een knop komen te staan waarmee we een nieuwe taak kunnen toevoegen. Het is vanzelfsprekend dat we opnieuw een herbruikbare component bouwen.
We kunnen de Pressable component dus gebruiken om een custom IconButton component te bouwen. De startbestanden bevatten al een begin voor deze component, we moeten enkel de Pressable wrapper toevoegen.
import {Pressable, StyleSheet, View} from 'react-native'
interface IconButtonProps {
onPress: () => void
}
const IconButton: FunctionComponent<IconButtonProps> = ({onPress}) => {
return (
<Pressable onPress={onPress}>
<View style={[styles.button]} />
</Pressable>
)
}Het is natuurlijk mogelijk om een knop te bouwen waarin gewoon de tekst 'New task' staat, maar dit is niet bepaald een aantrekkelijke UI. Daarom gebruiken we de icon library van Lucide React. Voor een lijst van alle beschikbare iconen kan je lucide.dev/icons raadplegen.
pnpm add lucide-react-nativenpm install lucide-react-nativebun add lucide-react-nativeyarn add lucide-react-nativeWe willen de IconButton natuurlijk zo generiek mogelijk kunnen gebruiken. Zo kunnen we van verschillende iconen een knop maken. We gebruiken hiervoor de LucideIcon en bijhorend LucideProps componenten uit lucide-react-native library. Icon is hier een React component die als Icon={SomeComponent} meegegeven moet worden. Omdat de component doorgegeven wordt zonder </> kunnen we het lucide-icoon in de IconButton component oproepen en de properties instellen.
import {LucideIcon, LucideProps} from 'lucide-react-native';
interface IconButtonProps extends LucideProps {
onPress?: () => void
Icon: LucideIcon
}
const IconButton: FunctionComponent<IconButtonProps> = ({onPress, Icon, ...iconProps}) => {
const {textColor} = useContext(ThemeContext)
return (
<Pressable onPress={onPress}>
<View style={[styles.button]}>
<Icon size={30} color={textColor} {...iconProps} />
</View>
</Pressable>
)
}const TaskPage: FunctionComponent = () => {
const {backgroundColor, surfaceColor} = useContext(ThemeContext)
return (
<SafeAreaProvider>
<SafeAreaView style={[{backgroundColor: surfaceColor}, styles.container]}>
<Header title="Tasks">
<IconButton onPress={() => alert('Works')} Icon={PlusIcon} />
</Header>
<View style={[styles.container, {backgroundColor}]}>
<StyledText>Een eerste React Native App</StyledText>{' '}
</View>
</SafeAreaView>
</SafeAreaProvider>
)
}MMKV
Voordat we de taken kunnen uitlezen, moeten we deze ergens persistent bewaren.
Begrip: MMKV
MMKV is een zeer snelle (en kleine) key-value storage library ontwikkeld door het Chinese WeChat. Deze library presteert in benchmarks aanzienlijk beter de SharedPreferences en UserDefaults, die respectievelijk voorzien worden door Android en iOS.
MMKV kan gebruikt worden om kleine, niet relationele, data te bewaren of om het resultaat van een HTTP request lokaal te cachen. Via de react-native-mmkv library kan MMKV gebruikt worden in een React Native applicatie.
Via de useMMKV... hooks kunnen we (een stuk van) een object of een primitieve waarde bewaren.
const Foo: FuctionComponent = () => {
const [someString, setSomeString] = useMKKVString('sleutel1')
const [someBoolean, setSomeBoolean] = useMKKVBoolean('sleutel2')
const [someNumber, setSomeNumber] = useMKKVNumber('sleutel3')
const [someObject, setSomeObject] = useMKKVObject<T>('sleutel4')
return <> {/* JSX-Code */} </>
}Naast MMKV installeren we ook de expo-crypto library om een UUID te generen voor elke taak.
pnpm expo install react-native-mmkv expo-cryptonpx expo install react-native-mmkv expo-cryptobun expo install react-native-mmkv expo-cryptoyarn expo install react-native-mmkv expo-cryptoMMKV is niet bedoeld voor grote en complexe read-operaties, daarom is deze library synchroon. Je zoek dus beter naar een andere oplossing als je complexere queries wilt uitvoeren of met grotere data wilt werken. Maar langs de andere kant betekent dit dat het niet nodig is om TanStack Query te gebruiken. We kunnen dus een eenvoudige hook schrijven die de taken uitleest, CRUD methodes voorziet en eventueel demo taken aanmaakt.
De code voor deze hook is grotendeels gegeven, we moeten de useState hook enkel vervangen met de useMMKVObject hook.
import ITask from '../models/ITask'
import {useMMKVObject} from 'react-native-mmkv'
import {useEffect} from 'react'
import {randomUUID} from 'expo-crypto'
type useTaskReturnValue = {
tasks: ITask[]
createTask: (name: string) => void
toggleTaskStatus: (id: string) => void
}
const useTasks = (key: string = 'tasks'): useTaskReturnValue => {
const [tasks, setTasks] = useMMKVObject<ITask[]>(key)
const isUndefined = tasks === undefined
useEffect(() => {
if (!tasks) {
const demoTasks: ITask[] = []
for (let i = 1; i <= 50; i++) {
demoTasks.push({
name: `Task ${i}`,
id: randomUUID(),
completed: i % 2 === 0,
})
}
setTasks(demoTasks)
}
}, [isUndefined])
const createTask = (name: string) => {
setTasks([
...(tasks ?? []),
{
name,
completed: false,
id: randomUUID(),
},
])
}
const toggleTaskStatus = (id: string) => {
const task = tasks?.find(t => t.id === id)
if (task) {
task.completed = !task.completed
setTasks(tasks)
}
}
return {
tasks: tasks ?? [],
createTask,
toggleTaskStatus,
}
}Tenslotte kunnen we deze hook dan gebruiken in de ToDoList component die gerenderd wordt als kind van de TaskPage. In ToDoList tonen we elke taak via een ToDoItem component. Zowel de ToDoList als ToDoItem component zijn beschikbaar in de startbestanden.
const TaskPage: FunctionComponent = () => {
const {backgroundColor, surfaceColor} = useContext(ThemeContext)
return (
<SafeAreaProvider>
<SafeAreaView style={[{backgroundColor: surfaceColor}, styles.container]}>
<Header title="Tasks">
<IconButton onPress={() => alert('Works')} Icon={PlusIcon} />
</Header>
<View style={[styles.container, {backgroundColor}]}>
<ToDoList />
</View>
</SafeAreaView>
</SafeAreaProvider>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
margin: 5,
flex: 1,
},
})const ToDoList: FunctionComponent = () => {
const {tasks} = useTasks()
return (
<>
{tasks.map(task => <ToDoItem {...task} key={task.id}/>)}
</>
)
}Zodra we deze code uitvoeren, zien we dat Metro (de bundler) een foutmelding uitprint.
Om dit probleem op te lossen, moeten we een native Android code generen en deze lokaal compileren. Via onderstaand commando wordt de Android code gegenereerd (en iOS code als je op macOS werkt).
pnpm expo prebuildnpx expo prebuildbun expo prebuildyarn expo prebuildNadat we dit commando uitvoeren, krijgen we de vraag om het package name in te geven (of wordt dit automatisch gegenereerd en in app.json toegevoegd).
Begrip: Package name
Het package id van je app is een wereldwijd unieke naam die gebruikt wordt om te bepalen of een app al geïnstalleerd is op een toestel en voor een hele reeks andere doeleinden.
Het package id moet in reverse domain name notation formaat genoteerd worden. Als je een app bouwt met als doel deze te publiceren, in plaats van om te leren, is het natuurlijk belangrijk dat je hiervoor een domeinnaam gebruikt die je effectief bezit. Domeinnamen zijn uniek en twee apps zullen dus nooit als "dezelfde app" beschouwd worden.
Expo bewaart de package name in app.json, je kan de naam altijd aanpassen in deze file.
Notitie
Gebruik tijdens deze cursus een appId van de vorm com.achternaam.voornaam.appnaam. De apps die als voorbeeld aangeboden worden krijgen steeds een id van de vorm be.pitgraduaten.appnaam.
De uitvoer van het prebuild commando vraagt ook om de expo-system-ui library te installeren, om van de waarschuwing af te geraken installeren we dit best.
pnpm expo install expo-system-uinpx expo install expo-system-uibun expo install expo-system-uiyarn expo install expo-system-uiNa deze aanpassingen kunnen we het project opnieuw compileren, maar dit keer met een ander commando dat de native code compileert in de plaats van Expo Go te gebruiken.
pnpm androidnpm run androidbun androidyarn run androidHet compilatieproces kan relatief lang duren, zeker de eerste keer.
Waarschuwing
Op het moment van schrijven (1/11/2024) mislukt het compilatieproces als je de blank template gebruikt. De expo-splash-screen library moet geïnstalleerd worden om de app succesvol te compileren.
pnpm expo install expo-splash-screennpx expo install expo-splash-screenbun expo install expo-splash-screenyarn expo install expo-splash-screenScrollView & List
De applicatie start nu wel op, maar de UI ziet er niet uit. De elementen in de lijst zijn volledig samengedrukt, zoals onderstaand screenshot demonstreert.

ScrollView
Het probleem is dat een React Native UI niet automatisch doorgroeit met de inhoud zoals op een website. De inhoud van een pagina wordt samengedrukt totdat deze op het scherm past. Om dit probleem op te lossen moeten we de ToDoList componenten wrappen in een ScrollView.
import {ScrollView} from 'react-native'
const ToDoList: FunctionComponent = () => {
const {tasks} = useTasks()
return (
<ScrollView>
{tasks.map(task => <ToDoItem {...task} key={task.id}/>)}
</ScrollView>
)
}Na deze aanpassing worden de taken correct getoond.

LegendList
Alhoewel de taken nu wel zichtbaar zijn, is de code niet optimaal. De ScrollView component gebruik je best enkel voor content die niet in een lijst weergegeven kan worden, dingen zoals een lange post op een social media platform. Voor content die volledig in een lijst weergegeven kan worden gebruik je best een list component.
De FlatList component rendert enkel de elementen die op het scherm staan en enkele extra elementen boven en onderaan het scherm (zodat scrollen vlot gaat). Omdat er minder elementen gerenderd worden is dit natuurlijk veel efficiënter, zeker voor grote lijsten.
Toch gebruiken we de FlatList component niet omdat deze bij grote hoeveelheden data, of op low-end Android toestellen, niet performant is. Shopify heeft dit probleem ook ondervonden en heeft als oplossing de FlashList component gebouwd. Ondertussen heeft een andere ontwikkelaar nog een verbeterde versie ontwikkeld genaamd LegendList. Wij zullen dus gebruik maken van de LegendList component voor zijn optimale performantie. De API voor deze component is vrijwel identiek aan die van FlatList en FlashList.
Zowel de FlatList als LegendList componenten ontvangen hun data via de data property, de renderItem property definieert een functie die één item uit de data array binnen krijgt en rendert naar een ReactNode. Verder gebruiken we de keyExtractor property om het default gedrag van de component te overschrijven, standaard Tenslotte geven wij de recycleItems property de waarde true. Deze property zorgt er voor dat componenten niet telkens worden verwijderd (uit het zicht) en opnieuw aangemaakt (net buiten zicht), in plaats daarvan worden de componenten herbruikt met nieuwe data.
Info
Als je data geen key of id property bevat, moet je een keyExtractor functie meegeven aan de FlatList, FlashList of LegendList component die een unieke key teruggeeft voor elk item in de lijst. Deze key heeft dezelfde functionaliteit als de key property die we in frontend les 2. besproken hebben.
Info
Als de componenten in de lijst interne state bevatten, moet je de recycleItems property op false zetten, anders kunnen er onverwachte bugs optreden. Als alternatief kan je ook gebruik maken van een useEffect in de component om de interne state te resetten telkens de props wijzigen.
pnpm expo install @legendapp/listnpx expo install @legendapp/listbun expo install @legendapp/listyarn expo install @legendapp/listimport {LegendList} from "@legendapp/list"
const ToDoList: FunctionComponent = () => {
const {tasks} = useTasks()
return (
<LegendList
data={tasks}
renderItem={({item: task}) => <ToDoItem {...task} key={task.id} />}
recycleItems={true}
/>
)
}Taak toevoegen
Het uitgewerkte lesvoorbeeld bevat, naast bovenstaande code, ook nog code om een taak toe te voegen. Deze code wordt opgebouwd met behulp van de Modal en TextInput componenten, maar bevatten verder geen nieuwe concepten. Daarnaast zijn er ook nog enkele hulpcomponenten toegevoegd, we raden geïnteresseerde lezer aan om deze code te bekijken vooraleer aan de oefeningen te beginnen.
Voorbeeldcode
Volledig uitgewerkt lesvoorbeeld met commentaar
Als dit niet het geval is, heb je iets fout gedaan tijdens de installatie van de development environment. ↩︎
Met de release van Expo DOM Components is het mogelijk componenten te maken met HTML code. LET OP: omdat dit in de achtergrond een WebView proxy component maakt, neemt dit net de kracht van native ontwikkeling weg (zie inleiding). Het is echter een handige tool om incrementeel een bestaande website om te zetten naar een app, of om enkele functionaliteiten uit te proberen voor je er veel tijd in stopt door ze native te maken. Je kan de volledige uitleg vinden op Expo DOM Components. ↩︎
Let op, React Native bevat ook een SafeAreaView component, maar deze werkt enkel op iOS. ↩︎
We gaan ervan uit dat flexbox gekend is, diegenen die een refresher nodig hebben verwijzen we door naar MDN en CSS-tricks. Om het resultaat van de verschillende opties interactief te bekijken, verwijzen we naar de React Native documentatie. ↩︎
In deze situatie is het ook mogelijk om de TextProps interface te importeren uit React Native, maar dit is niet voor alle componenten uit alle libraries mogelijk. De ComponentProps helper is een generieke oplossing die altijd werkt. ↩︎