LAB02

React & React Native: Syntax, Hooks & Your First Multi-Page App

Learn React and React Native through 17 interactive exercises, study a complete Expo Router app, then build and publish your own four-screen Movies watchlist.

Beginner 3 hoursjsxcomponentspropsusestateuseeffectuserefusememoreact-nativenativewindflatlistexpo-routercontext

Your progress

0 / 17 exercises

11 blocks · 165 min

Before you start

Who’s doing Lab 02?

Waiting for your TA to start this lab session. You can read the content in the meantime.

By the end you will

  • Explain how React starts, how JSX works, and why apps are built from components
  • Pass data with props and render conditions and keyed lists
  • Use useState, useEffect, useRef and useMemo correctly
  • Build phone screens with View, Text, Image, TextInput, Pressable and FlatList
  • Style with flexbox and NativeWind classes
  • Navigate with Expo Router using stacks, tabs and route parameters
  • Share state between screens with Context
  • Build a four-screen Movies app and merge it through GitHub

You need

  • Lab 01 completed: Expo runs on your phone and Git is configured
  • A laptop with Chrome, Edge or Firefox for the interactive exercises
  • Expo Go on your phone, or the Android emulator, for the final project
  • Your GitHub account and token from Lab 01

Tip

The first half is your fast practice loop: edit, run and check code in the browser. The second half moves the same ideas into a real Expo project on your phone.

Part 1: How a React app starts

10 min

What is React?

Components

You build the UI from small, reusable pieces — like LEGO.

Describe, don’t draw

You say what the screen should show for the current data. React updates the screen for you.

Learn once

React for the web, React Native for phones — same ideas, same code style.

Where does my code start?

React · web
  1. index.htmlan empty <div id="root">
  2. main.tsxcreateRoot(#root)
  3. <App />the root component
  4. children<Header /> <ProductCard />
  5. UIdrawn in the browser
Expo · phone
  1. expo-routerstarts the app
  2. app/_layout.tsxthe root layout
  3. app/index.tsxthe first screen
  4. children<Header /> <ProductCard />
  5. UIreal native views
Same idea everywhere: an entry file finds a place to draw → renders the root component → which renders its children → UI.
● LIVE DEMOHow a React web app startsLoading the editor and preview…
● LIVE DEMOHow an Expo app startsLoading the editor and preview…
EX 01Order the app start-upLoading the exercise…

Part 2: Components

10 min

A component is a function that returns UI

function Greeting() {
  return <h2>Hello, React!</h2>;
}

// use it like a tag — as many times as you want:
<Greeting />
<Greeting />

Capital letter

Greeting, never greeting — lowercase means an HTML tag.

Returns JSX

HTML-like code inside JavaScript, wrapped in one parent element.

Compose them

Components use other components: App → Header, ProductCard…

<App />the root component
  1. <Header />title + cart
  2. <SearchBar />input
  3. <ProductCard />props: 🎧 Headphones
  4. <ProductCard />props: ⌚ Smart watch
  5. <TabBar />navigation
Same component, different data: both product boxes are one ProductCard — the data they show comes in as props.
EX 02Split a screen into componentsLoading the exercise…
EX 03Your first componentLoading the exercise…

Part 3: Props & hooks

10 min

Props — data from parent to child

PARENT · App

<ProfileCard name="Mariam" role="Student" /><ProfileCard name="Ali" role="Teaching assistant" /><ProfileCard name="Youssef" role="Instructor" />

CHILD · ProfileCard

function ProfileCard({ name, role })
MariamStudent
AliTeaching assistant
YoussefInstructor
The parent decides the data, the child decides how it looks. Props only flow down — a child can’t change its own props.
● LIVE DEMOProps: one component, different dataLoading the editor and preview…
EX 04Reusable ProfileCard with propsLoading the exercise…

State — React’s memory (useState)

1TaponPress / onClick runs
2setCount(count + 1)the setter saves the new value
3React re-renderscalls App() again with count = 1
4Screen updatesCount: 1 is drawn
State is React’s memory. Changing it with the setter is the only way to make the screen update — a normal variable would be forgotten on the next render.

useEffect — do something after the screen is drawn

render 1first renderrender 2count changedrender 3name changedrender 4count changed
useEffect(fn)no array → after every render●●●●
useEffect(fn, [])empty → once, after the first render●–––
useEffect(fn, [count])first render + whenever count changes●●–●
An effect runs after React draws the screen. The dependency array decides when it runs again. Use it for “side effects”: timers, fetching data, logging.

useRef

Keeps a value between renders without re-rendering.

useMemo

Remembers a slow calculation.

useCallback

Remembers a function between renders.

useContext

Shares data with many components (e.g. the logged-in user).

Note

Hooks are functions starting with use that give components extra powers. Call them at the top of a component — never inside an if or a loop. Today you only need useState and useEffect.

● LIVE DEMOuseState & useEffectLoading the editor and preview…
EX 05useState counterLoading the exercise…
EX 06When does useEffect run?Loading the exercise…

The hook details you will need in a real app

useEffect runs after React draws the screen. Its dependency array controls when it runs:

You writeIt runs
useEffect(fn)after every render
useEffect(fn, [])once, after the first render
useEffect(fn, [query])after the first render and whenever query changes
return () => ... inside fnbefore the effect runs again and when the component unmounts
const [movies, setMovies] = useState<Movie[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetch('https://example.com/movies.json')
    .then((response) => response.json())
    .then(setMovies)
    .finally(() => setLoading(false));
}, []);

useRef returns a box whose .current value survives renders. Attach it to an input when you need to focus that input, or use it for a value that should not redraw the screen.

const titleRef = useRef<TextInput>(null);

function save() {
  if (!title.trim()) {
    titleRef.current?.focus();
    return;
  }
}

<TextInput ref={titleRef} value={title} onChangeText={setTitle} />

useMemo remembers the result of a calculation until one of its inputs changes. It is useful for filtering a large list, but ordinary small calculations do not need it.

const filteredMovies = useMemo(
  () => movies.filter((movie) => movie.title.toLowerCase().includes(query.toLowerCase())),
  [movies, query],
);

Important

Call hooks only at the top level of a component or custom hook. Never put a hook inside an if, loop, event handler, or nested function.

Conditions, lists and stable keys

Use a ternary when exactly one of two things should appear, and && when something is optional:

{loading ? <ActivityIndicator /> : <MovieList movies={movies} />}
{error && <Text className="text-red-600">{error}</Text>}

When .map() creates components, each outer element needs a unique, stable key. Do not use the array index when items can be inserted, removed or reordered.

{movies.map((movie) => <MovieCard key={movie.id} movie={movie} />)}

Part 4: React Native components

10 min

Same React — native building blocks

React · webReact Native · phone
  1. <div><View>a box / layout
  2. <p>, <h1>, <span><Text>all words live here
  3. <img src><Image source>pictures (give a size!)
  4. <button onClick><Pressable onPress>anything tappable
  5. <input onChange><TextInput onChangeText>typing
  6. CSS / classNamestyle={{ }} · NativeWindstyling
Components, props, state and hooks stay exactly the same. Only the building blocks change — and they become real native views on Android and iOS.
● LIVE DEMOThe core React Native componentsLoading the editor and preview…
EX 07Match the core componentsLoading the exercise…
EX 08Profile screenLoading the exercise…

Part 5: Inputs & state

10 min

A TextInput is controlled by state

const [name, setName] = useState('');

<TextInput value={name} onChangeText={setName} placeholder="Your name" />
<Text>Hello, {name}!</Text>

value

What the box shows — it comes from state.

onChangeText

Called with the new text on every keystroke.

Show / hide

Pick the UI with a condition: {name === '' ? <A /> : <B />}

● LIVE DEMOTextInput + stateLoading the editor and preview…
EX 09Fix the React Native bugsLoading the exercise…
EX 10Live greeting with TextInputLoading the exercise…

Part 6: Styling & NativeWind

10 min

Styling in React Native

No CSS files

Styles are JavaScript objects: style={{ padding: 16 }}.

camelCase

backgroundColor, not background-color.

Plain numbers

16 = 16 density-independent pixels. No px.

Flex by default

Every View is a flexbox column.

flexDirection: 'column'the default in React Native
justifyContent · main axisalignItems · cross axisABC

justifyContent moves along the vertical ↓ · alignItems along the horizontal →

flexDirection: 'row'className="flex-row"
justifyContent · main axisalignItems · cross axisABC

justifyContent moves along the horizontal → · alignItems along the vertical ↓

On the web a div lays children out in a row; in React Native every View is a flex column by default.
EX 11Flexbox playgroundLoading the exercise…

NativeWind — Tailwind classes for React Native

What it is

Write short utility classes in className — NativeWind turns them into React Native styles.

Why use it

Faster to write, consistent spacing and colours, and you can read the design straight from the code.

<View className="flex-rowitems-centergap-3bg-whiterounded-2xlp-4shadow-md">
  1. flex-rowflexDirection: 'row'
  2. items-centeralignItems: 'center'
  3. gap-3gap: 12
  4. bg-whitebackgroundColor: '#ffffff'
  5. rounded-2xlborderRadius: 16
  6. p-4padding: 16
  7. shadow-mdshadowColor: '#000', shadowOffset: {"width":0,"height":4}, shadowOpacity: 0.1, shadowRadius: 6, elevation: 4
● LIVE DEMOStyle objects vs NativeWindLoading the editor and preview…

Tip

In your own Expo project, follow the official NativeWind install guide once (about 5 minutes) — then className works exactly like it does here.

EX 12Match the NativeWind classesLoading the exercise…
EX 13Style a card with NativeWindLoading the exercise…

Part 7: Lists

10 min

Arrays → rows with FlatList

ScrollView renders every child at once — fine for one page of content.

FlatList renders only what’s on screen and creates more rows as you scroll — smooth even with 10,000 items.

<FlatList data={STUDENTS} renderItem={({ item }) => <Row name={item.name} />} />
  • data — the array
  • renderItem — one item → one component
  • keyExtractor — which field is unique
● LIVE DEMOFlatList renders only what you seeLoading the editor and preview…
EX 14Assemble a FlatList screenLoading the exercise…
EX 15Course list with FlatListLoading the exercise…

Part 8: Putting it together

10 min

One more idea: function props

A child can’t change the parent’s state — but the parent can give it a function to call.

// parent (App): owns the state, passes a function down
<ContactCard name={c.name} favourite={favs.includes(c.id)} onToggle={() => toggle(c.id)} />

// child (ContactCard): calls it when tapped
<Pressable onPress={onToggle}><Text>{favourite ? '★' : '☆'}</Text></Pressable>

Data goes down

Props carry values from parent to child.

Events go up

Function props let the child tell the parent “I was tapped”.

State lives on top

Keep state in the closest parent that needs it.

EX 16Contacts: search & favouritesLoading the exercise…

Part 9: Final challenge

10 min

Build a task list from scratch

Everything you practised today, in one small feature. The checklist is your spec — it fills in as you meet each requirement. Hints unlock one at a time; there are no solutions to copy.

EX 17Final: task list appLoading the exercise…

Part 10: Navigation + the Recipes app

30 min

The browser exercises taught each idea separately. Now see how those ideas connect in a real multi-screen Expo app before building your own.

TASK 10.1

Routing on mobile: Expo Router

7 min

Expo Router maps files to screens. Every file inside app/ becomes a route; folders organise the routes, and layout files decide whether screens use a stack or tabs.

app/ → routes
  • app/
  • _layout.tsxwraps every route in a Stack
  • (tabs)/route group; not part of the URL
  • _layout.tsxbottom tab bar
  • index.tsx→ /
  • favorites.tsx→ /favorites
  • add.tsx→ /add
  • recipe/
  • [id].tsx→ /recipe/1, /recipe/2 ...

Stack

Pages slide on top of one another and get a Back button.

<Stack />

Tabs

A bottom bar switches between the main areas of the app.

<Tabs />

Navigate

Navigate in code, or wrap content in <Link href>.

router.push('/add')
<Link href={`/recipe/${recipe.id}`} asChild>
  <Pressable>
    <Text>{recipe.title}</Text>
  </Pressable>
</Link>
TASK 10.2

Dynamic routes and route parameters

6 min

Square brackets create a dynamic page. app/recipe/[id].tsx handles /recipe/1, /recipe/2, and every other recipe id.

import { Stack, useLocalSearchParams } from 'expo-router';

export default function RecipeDetails() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const recipe = recipes.find((item) => item.id === id);

  if (!recipe) return <Text>Recipe not found</Text>;

  return (
    <View>
      <Stack.Screen options={{ title: recipe.title }} />
      <Text>{recipe.title}</Text>
    </View>
  );
}

Warning

Route parameters are strings. If your data uses numeric ids, compare with item.id === Number(id).

TASK 10.3

Share state between pages with Context

7 min

The Details page changes favorites, while the Favorites tab displays them. State owned by either page would disappear from the other, so place it in a provider above both screens.

type RecipesValue = {
  favorites: string[];
  toggleFavorite: (id: string) => void;
};

const RecipesContext = createContext<RecipesValue | null>(null);

export function RecipesProvider({ children }: PropsWithChildren) {
  const [favorites, setFavorites] = useState<string[]>([]);
  const toggleFavorite = (id: string) =>
    setFavorites((current) =>
      current.includes(id) ? current.filter((item) => item !== id) : [...current, id],
    );

  return <RecipesContext.Provider value={{ favorites, toggleFavorite }}>{children}</RecipesContext.Provider>;
}

export function useRecipes() {
  const value = useContext(RecipesContext);
  if (!value) throw new Error('useRecipes must be inside RecipesProvider');
  return value;
}

Wrap the Stack with <RecipesProvider> in app/_layout.tsx. Any screen below it can then call useRecipes().

TASK 10.4

Walk through the complete Recipes app

10 min

This is the complete four-page example from the original Lab 02. Select a file to read its code; use the phone preview to see how the files work together.

The root layout: a Stack (pages slide on top, with Back), wrapped in RecipesProvider so every page shares the same data (Task 5.3).

import { Stack } from 'expo-router';
import { RecipesProvider } from '../context/recipes';

// Root layout: a Stack (pages slide on top of each other, with a Back button),
// wrapped in the provider so every page shares the same recipes and favorites.
export default function RootLayout() {
  return (
    <RecipesProvider>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="recipe/[id]" options={{ title: 'Recipe' }} />
      </Stack>
    </RecipesProvider>
  );
}

Starting…

● live: tap around

Find the code responsible for each behaviour:

  1. Search for fal. Which hook calculates the filtered list, and what are its dependencies?
  2. Open Koshari, add it to favorites, then open the Favorites tab. Which provider makes that possible?
  3. Press Save recipe with an empty title. Where is the error set, and how does the ref focus the title input?
  4. Add a recipe. Which function creates the new array, and why would recipes.push(...) be wrong?

Part 11: Your project: a four-screen Movies app

45 min

Build the same architecture yourself in a real Expo project. The specifications below are deliberately detailed, but the implementation is yours.

TASK 11.1

Create and organise the project

7 min

The tabs template already includes Expo Router, a Stack and a tab bar.

Terminal — create the project
npx create-expo-app@latest movies-app --template tabs
cd movies-app
git switch -c feature/movies-app
npx expo start

Reshape the template into this structure:

movies-app
  • app/
  • _layout.tsxStack + MoviesProvider
  • (tabs)/
  • _layout.tsxMovies · Watchlist · Add
  • index.tsxPage 1: Movies
  • watchlist.tsxPage 3: Watchlist
  • add.tsxPage 4: Add movie
  • movie/
  • [id].tsxPage 2: Movie details
  • components/
  • MovieCard.tsxreceives one Movie prop
  • context/
  • movies.tsxshared list + watchlist state
  • data/
  • movies.tsstarting data + Movie type
export type Movie = {
  id: string;
  title: string;
  year: number;
  genre: string;
  rating: number;
  emoji: string;
};

export const MOVIES: Movie[] = [
  { id: '1', title: 'Inception', year: 2010, genre: 'Sci-Fi', rating: 8.8, emoji: '🌀' },
  { id: '2', title: 'The Lion King', year: 1994, genre: 'Animation', rating: 8.5, emoji: '🦁' },
  { id: '3', title: 'Interstellar', year: 2014, genre: 'Sci-Fi', rating: 8.7, emoji: '🪐' },
  { id: '4', title: 'Spirited Away', year: 2001, genre: 'Animation', rating: 8.6, emoji: '🐉' },
  { id: '5', title: 'The Dark Knight', year: 2008, genre: 'Action', rating: 9.0, emoji: '🦇' },
  { id: '6', title: 'Coco', year: 2017, genre: 'Animation', rating: 8.4, emoji: '💀' },
];
TASK 11.2

Page 1: Movies list and search

9 min
app/(tabs)/index.tsx

Your page must include:

  • A FlatList of every movie with a stable keyExtractor.
  • A reusable MovieCard receiving one typed movie prop.
  • Emoji, title, year and rating on each card.
  • A Link with asChild that opens /movie/<id> when the card is pressed.
  • A controlled search box and a useMemo filtered list.
  • A useful empty result such as “No movie matches…”.
Terminal
git add .
git commit -m "Page 1: movies list with search"
TASK 11.3

Page 2: Movie details

9 min
app/movie/[id].tsx

Your page must:

  • Read id with useLocalSearchParams and find the corresponding movie.
  • Display its emoji, title, year, genre and rating.
  • Set the Stack header title to the movie title.
  • Toggle between Add to watchlist and Remove from watchlist.
  • Render “Movie not found” for an invalid id.

The watchlist is used here and on Page 3, so own it in context/movies.tsx, expose it with useMovies(), and wrap the app in MoviesProvider.

Terminal
git add .
git commit -m "Page 2: movie details and watchlist toggle"
TASK 11.4

Page 3: Watchlist

7 min
with movies
empty state

Reuse MovieCard, show only saved movies, include the count with correct singular/plural grammar, and provide a friendly empty state.

Terminal
git add .
git commit -m "Page 3: watchlist"
TASK 11.5

Page 4: Add movie form

8 min
app/(tabs)/add.tsx

Your form must include:

  • Controlled title, year and genre inputs.
  • Validation: title is required; year must be a number from 1900 to 2030.
  • A red error message and useRef to focus the field that needs fixing.
  • A save handler that creates a new movie without mutating the old array.
  • Cleared inputs and router.push('/') after a successful save.
Terminal
git add .
git commit -m "Page 4: add movie form with validation"
TASK 11.6

Test, push and merge

5 min

Before pushing, test this complete journey on your phone:

  1. Search for a movie and open its details.
  2. Add it to the watchlist and confirm it appears in the Watchlist tab.
  3. Remove it and confirm the empty state returns.
  4. Submit the Add form empty and confirm validation focuses the title.
  5. Add a valid movie and confirm it appears on the Movies page.
  6. Open an invalid details URL and confirm the app does not crash.
you are here
Push the feature branch
git remote add origin https://github.com/your-username/movies-app.git
git push -u origin feature/movies-app
feature/movies-app -> feature/movies-app

On GitHub choose Compare & pull request → Create pull request → Merge pull request. Then update the local copy:

Terminal
git switch main
git pull
main now contains all four pages

Paste the repository link for your TA:

Mission complete

Your Movies app has four working pages, shared watchlist state, search, validation, route parameters, and a reviewed Git history merged into main. 🎬

Stuck?

"Text strings must be rendered within a <Text> component"
Cause

Text was placed directly inside a View.

Fix

Wrap every piece of text in <Text>…</Text>.

"Each child in a list should have a unique key prop"
Cause

A .map() returns items without a stable key.

Fix

Add key={item.id}, or use keyExtractor on a FlatList.

"Too many re-renders"
Cause

State is changed while rendering, such as onPress={setCount(1)}.

Fix

Pass a function instead: onPress={() => setCount(1)}.

"Rendered more hooks than during the previous render"
Cause

A hook is inside an if, loop, nested function, or after an early return.

Fix

Call every hook at the top of the component in the same order.

The app fetches or re-renders forever
Cause

A useEffect changes state but has a missing or incorrect dependency array.

Fix

Use [] for once, or list every reactive value the effect reads.

"Unmatched route" or the page is not found
Cause

The file is outside app/, or the link does not match its file name.

Fix

app/movie/[id].tsx is opened with /movie/3; check both spellings.

Expo Go keeps loading or cannot connect
Cause

The phone and laptop are on different networks, or the network blocks discovery.

Fix

Stop the server and start it in tunnel mode.

Run
npx expo start --tunnel

Still stuck? Search the troubleshooting center or ask your TA.

Submission

  • All 17 interactive exercises show Solved
  • A link to the movies-app GitHub repository with feature/movies-app merged into main
  • A screenshot of each of the four pages running on your phone
Optional

Extra challenge

Persist the watchlist with @react-native-async-storage/async-storage: load it once at startup and save it whenever it changes.

Resources

End of lab · attendance

Check in for Lab 02

Checking whether check-in is open…