Notes on learning stage of developing my own React Native education app using Expo Framework - Part 5

Last updated on 19 Oct. 2025

Main tutorial used for learning: Simon Grimm: Build Your Own MASTERCLASS clone in React Native, https://www.youtube.com/watch?v=fO3D8lNs10c , 4 hrs. 42 mins, Mar. 2025.

=======================================

13:30, 18 October 2025
To do (from older notes):
  • After above, production deploy can be considered. With prod. deploy change below env var in .env.cloud.fs:
    • # EXPO_PUBLIC_WEB_SOON_URL=http://10.50.171.151:8081/soon
To do (new):
  • Done by dropping window width change event altogether: Debounce window width change event handling (comment out console.log)
  • In Browse courses -> particular course -> course page  and in Course overview page:
    • The image at the top gets vertically clipped by a large amount when window width is large like on PC desktop full window width case. It is not a big issue but if the same solution to above issue which was applied in home page can be easily applied here, it may be worth trying it out.
  • Done: In Mobile web view: In Portrait mode, Browse Courses and My Courses screen have course cards which are horizontally clipped. In landscape mode, this clipping issue is not there.
  • In Mobile Android app: Browse Courses screen extends into status bar. [But Course Overview screen does not do so.]
  • In Mobile Android app, Browse Courses screen can be dragged down to close the screen. That seems to be the only screen which has this feature. Is it something SG used on this screen alone to demonstrate the feature? In web app, Browse Courses screen cannot be dragged (mouse click-and-drag) in any direction.
  • Done: In Mobile web app, lesson screen in landscape mode shows drawer in open state on the left which takes up lot of space. The video box is very small. In Portrait view, video box is quite Ok and is only slightly less than in mobile android app, and the drawer is in collapsed state.
    • Even in desktop web app, I think the open drawer size can be reduced but that I think should be considered only as a last resort.
------- end To Do list ---------------

Commit: Commented out not needed console.log statements across project

Commit: Improved Home screen: used max-width and dropped event handling code; Also use max width of 800px

================

[For HomeBlock.tsx: About Home screen doing elastic bounce on Android but not on web.]

Me: Oh I see! It is ScrollView on Native that does the bounce but which does not do the bounce on web.

ChatGPT (CG) said: Exactly ✅ — you’ve got it 100% right!

To restate in simple terms:

  • On Android and iOS, React Native’s <ScrollView> uses the native scrolling physics of each platform.

    • These include that “elastic overscroll bounce” when you pull down or up past the edges of the content.

    • That bounce is built into the native OS scrolling behavior, not your animation code.

  • On web, the same <ScrollView> is rendered using a regular HTML <div> with overflow scrolling — browsers typically don’t do that elastic bounce (except maybe on macOS Safari with trackpad “rubber-band” effect).

So:
🌀 The bounce = normal native ScrollView behavior.
🖥️ No bounce on web = normal web scroll behavior.
🎨 Your Animated.View fade-in only affects how the block appears — not scrolling physics.

If you ever want a uniform, no-bounce behavior across all platforms, just add:

bounces={false}
overScrollMode="never"

to your <ScrollView> — and the screen will feel firm on Android/iOS just like it already does on web.

================

CG (summary): 

useWindowDimensions() is a React Native hook that automatically provides the current screen width and height, and reactively updates whenever the screen size or orientation changes. Unlike Dimensions.get('window'), which only fetches the initial values and requires manual event listeners to handle updates, useWindowDimensions() is fully managed by React Native’s internal system. This means you don’t need to add resize listeners or debounce events — the hook efficiently updates your component only when necessary.
👉 In short: useWindowDimensions() is a smart, auto-updating, and debounced alternative to Dimensions.get(), perfect for responsive layouts with minimal code.

...

Note: Responsive Layouts in React Native — Dimensions.get() vs useWindowDimensions()

In early versions of our HomeBlock.tsx, we used Dimensions.get('window') along with a manual event listener to track window size changes and adjust image heights and layout. This gave us fine-grained control for experimentation but required extra code for subscribing, updating state, and debouncing events to avoid excessive re-renders.

React Native provides a simpler alternative: useWindowDimensions(). This hook automatically returns the current screen width and height and reactively updates whenever the screen size or orientation changes, without needing any event listeners or debouncing. It works on both mobile and web, making it ideal for responsive layouts.

In short: useWindowDimensions() is a smart, auto-updating, and debounced replacement for Dimensions.get(), perfect for clean and responsive UI code.


...

Lesson learned: When building responsive layouts in React Native, start with useWindowDimensions() for automatic, clean, and reactive screen-size handling — reserve Dimensions.get() with listeners only if you need very fine-grained, low-level control.

==================

We eventually could entirely drop window size related code in  HomeBlock.tsx. So the above note is a learning note for future.

Commit: Mobile web portrait mode: Fixed horizontal clipping of course cards in Browse Courses and My Courses screens
....

Me: In the following file: app\(app)\(authenticated)\course\[slug]\[lesson]\_layout.tsx I want to keep the drawerType as 'front' for special case of mobile web browser in landscape mode even if dimensions.width is > 768

[Long exchange on how to detect mobile web landscape mode. app\(app)\(authenticated)\course\[slug]\[lesson]\_layout-mob-lscape.tsx.txt has some of the attempts but they had some issue or the other. I have given below some key points from the exchange.]

// Detect mobile web
  const isMobileWeb =
    Platform.OS === 'web' &&
    (navigator.userAgent.match(/Android/i) ||
      navigator.userAgent.match(/iPhone|iPad|iPod/i));

Is that a good way to detect mobile web? Will all mobile browsers on Android or iOS provide correct userAgent info matching above code?
...

how about this approach:
  const { width, height } = useWindowDimensions();

  const isLandscape = width > height;
...

Even on PC, for large window width, it gets viewed as landscape.
So that test for mobile landscape is not good enough
...

In short:
  • Aspect ratio alone is insufficient.

  • Combine touch detection + orientation to detect mobile landscape reliably.

...
you can have a tablet instead of pc. So that too won't work.
...

I think it may be best to use user-agent as we do not have any other clear way
...

Just struck me that user agent may be Android or iOS for tablets too. So it is not a clear test for mobile. u agree?
...

Exactly — I completely agree. ✅
  • Both Android and iOS tablets report user-agent strings very similar to phones.

    • Example: An iPad in Safari often has iPad or even iPhone in older iOS versions, or “Macintosh” in newer ones with desktop mode.

    • Android tablets usually include Android in the UA, just like phones.

So just checking the user-agent cannot reliably distinguish phones from tablets.

  • That’s why a combination of user-agent + screen width (or maxTouchPoints) is often used in practice.

    • Phones: narrower width (≤ 768px)

    • Tablets: wider width (> 768px)

  • Even then, it’s still a heuristic — no method is 100% guaranteed.

In short: UA alone is not a clear or foolproof test for mobile phones vs tablets.

...

I think I will go with width > height AND height being less than some number that is suitable for mobile but not tablet.
...

I plan to use this:
  const dimensions = useWindowDimensions();
  const isMobileWebLandscape = Platform.OS === 'web' && dimensions.width > dimensions.height && dimensions.height < 500;
--------------
You see, my Samsung M21 shows 412 px (viewport px something not physical px) height in landscape mode

https://www.browserstack.com/guide/common-screen-resolutions says that in 2025:
Top Common Screen Resolutions Worldwide

Desktop: 1920×1080, 1366×768, 1536×864, 1280×720
Mobile: 360×800, 390×844, 393×873, 412×915
Tablet: 768×1024, 1280×800, 800×1280, 820×1180
--------------
So in mobile landscape mode, the height will typically be less than 500 px (viewport px).
Yr view please.
...

✅ Excellent reasoning — your approach and justification are solid.

Let’s analyze your detection line:

const dimensions = useWindowDimensions();
const isMobileWebLandscape =
  Platform.OS === 'web' &&
  dimensions.width > dimensions.height &&
  dimensions.height < 500;

👍 Why this is a sensible heuristic

  1. Platform.OS === 'web' – correctly limits detection to web builds.

  2. width > height – identifies landscape orientation.

  3. height < 500 – cleverly distinguishes mobiles in landscape from tablets or desktops.

📱 Real-world match

Your Samsung M21’s 412 px landscape height fits this perfectly.
Based on current (2025) viewport metrics:

  • Mobiles in landscape → 360–450 px height

  • Tablets in landscape → 600–800 px height

  • Desktops → typically >700 px height

So < 500 cleanly captures almost all phones while excluding tablets/desktops.

⚙️ Recommendation

You might want to define the threshold as a named constant for readability and easy tuning later:

const MOBILE_LANDSCAPE_MAX_HEIGHT = 500;
const isMobileWebLandscape =
  Platform.OS === 'web' &&
  dimensions.width > dimensions.height &&
  dimensions.height < MOBILE_LANDSCAPE_MAX_HEIGHT;

This makes it clear why the cutoff exists, and lets you tweak it if a specific device behaves differently.

✅ Summary

Your heuristic:

  • Works reliably across 2025’s common devices

  • Avoids false positives from tablets/desktops

  • Is simple and efficient (no dependency on user-agent parsing)

So yes — go ahead with this method. It’s a well-reasoned, practical, and future-resilient solution for detecting mobile web landscape mode.

...
An overview of what I did in past hour or so perhaps: I made changes to two files to get a sort-of optimal solution to this problem: On mobile web landscape mode the lesson screen's video element/control was very small.
So I first figured out how to detect that the device is using mobile web landscape mode (which I discussed above with you).
Next, if device is using mobile web landscape mode, I reduced the width of the permanently open drawer and increased the video height to 60% (from previous 40%).
This has made a significant improvement in lesson screen UI in mobile web landscape mode as the video component is no longer too small. Some lesson names in the drawer have last part of their names clipped but that's an acceptable compromise. There is a minor issue of lack of vertical scrolling in lesson screen due to which the button at the bottom is vertically clipped in mobile web landscape mode.
The behaviour of the app for desktop web, mobile web portrait mode as well as Android (Native) app is not changed.

Commit: Mobile web landscape mode: Increased lesson screen's video height and reduced drawer width to improve screen UI; Need to add vertical scrolling to lesson screen
....

[Then we used ScrollView instead of View in lessons screen. Parts of related exchange is given below.]

Commit: Added vertical scroll to lesson page; Initial testing is OK but needs more testing

09:18, 19 October 2025
[CG explanation on RN ScrollView and View differences.]

Me: i thought view and scrollview should handle the styling the same way.

CG: That’s a very natural assumption — but in React Native (and React Native Web), View and ScrollView differ in how they handle flex and layout. Here’s the key difference that caused what you observed 👇

🧩 1. View layout behavior

A plain <View>:

  • Fully participates in Flexbox layout.

  • Its children are laid out normally and fill space according to flex rules.

  • If you set flex: 1 on the View or its parent, it expands to fill available space.

So your “video + button at bottom” layout works perfectly in a View.


🌀 2. ScrollView layout behavior

A <ScrollView> is special:

  • It creates its own scrollable content container inside.

  • The outer ScrollView fills space like a normal View,
    but its inner content container (the scrollable part) sizes itself to fit the content.

  • That means if your children don’t naturally fill the height, it collapses — and flex behavior doesn’t apply as expected.

That’s why when you switched from ViewScrollView, the layout “broke” — the internal scroll container didn’t stretch to fill the height.


💡 3. Why contentContainerStyle={{ flexGrow: 1 }} fixes it

flexGrow: 1 tells the content container to expand and fill all available vertical space, even if its children don’t require it.
So:

  • When content is short → it fills the screen properly.

  • When content is long → it scrolls correctly.


✅ Bottom line

  • View → static layout (no scroll)

  • ScrollView → dynamic height, needs contentContainerStyle={{ flexGrow: 1 }} to behave like View

So yes — they look similar but their internal layout models differ subtly but importantly.
Your final version (with flexGrow: 1) makes the ScrollView behave like a full-height View plus scrolling support — the best of both worlds.


Me: Hmm. Plain React web does not have this issue, isn't it? Do we have a ScrollView in plain React?

CG: Excellent question — and you’re absolutely right to notice that this issue is specific to React Native (and React Native Web), not plain React for the browser.

Let’s unpack that clearly 👇


🧱 In plain React (web)

There’s no ScrollView component built in.
You just use regular HTML elements, mainly <div> or <section>, and make them scrollable with CSS:

<div style={{ overflowY: 'auto', height: '100vh' }}>
  {/* long content here */}
</div>

✅ This works perfectly because:

  • HTML layout uses block and CSS box model, not React Native’s Flexbox abstraction.

  • The scrollable area and the layout sizing are handled directly by the browser.

  • You don’t need a separate contentContainerStyle — the div itself is the scroll container.


📱 In React Native / React Native Web

ScrollView is a special React Native abstraction, designed to work across Android, iOS, and Web.
It simulates the scrollable div concept but with native-style behavior:

  • It renders a nested structure internally (an outer container + inner scroll content).

  • That inner content does not automatically fill its parent, because in native mobile rendering engines, scrollable areas are meant to size themselves to content first.

Hence why you need:

contentContainerStyle={{ flexGrow: 1 }}

—to mimic a “full height scrollable div” layout.


🧭 Summary comparison

Platform Scroll Container Behavior Needs flexGrow: 1?
Plain React (Web) <div style={{ overflowY: 'auto' }}> Natural scroll & layout ❌ No
React Native <ScrollView> Scroll container wraps content ✅ Yes
React Native Web <ScrollView> abstraction → <div> internally Mimics RN layout quirks ✅ Yes

So yes — you’re absolutely right:
👉 Plain React web doesn’t have this problem because it uses the browser’s natural scroll model.
The ScrollView abstraction introduces this complexity so the same code can behave consistently on both mobile and web.


------------

Me: About why I am making some effort to improve UX for mobile web landscape:
Even if Android app is available as an APK (but not on Play Store), some users may not want to install the app (especially as APK has to be side-loaded). 
Also some mobile users may be on iOS mobiles or even tablets but I don't have an iOS version of the app.
So some users may want to explore this app via mobile browser.

Now in mobile browser, portrait mode is quite OK for some app functionality.
But a key app functionality is playing the lesson video. On mobile, most users would want to play it full screen. Full screen mode on mobile switches automatically to landscape mode (as that is more suitable for typical aspect ratio videos I guess). So the user has to rotate the device. 
Now if the user stops the video and wants to navigate to another lesson using left drawer, as the device is in landscape mode now, it shows the mobile web landscape view. User will find it convenient to operate it in this landscape view itself instead of rotating device to portrait mode, choose another lesson, click full screen which takes device back to landscape mode making user rotate device again.
But if the mobile web landscape UX is reasonable, user could navigate through various lessons of the course (or another course) on landscape mode itself without having to do lot of device rotation back-and-forth.

What's ur view of my above analysis? Please be frank as I am new to mobile app development and so could be way off the mark here.

CG: Your analysis is actually very sound and practical, and shows good empathy for real users. Here’s a breakdown:
  1. APK vs browser: You correctly identified that not all users will install a side-loaded APK. iOS users can’t install your app at all yet, and some Android users may avoid side-loading. So mobile web access is important. ✅

  2. Portrait mode usability: Portrait works for some functionality, but for videos, the natural aspect ratio is landscape, so full-screen playback almost always rotates the device. Users expect this. ✅

  3. User frustration with rotation: You’re thinking ahead about friction. If a user rotates to landscape to watch a video, then has to rotate back to portrait just to navigate the lessons, that’s extra effort and frustration. Making the landscape UX usable for navigation is smart UX thinking. ✅

  4. Mobile web landscape UX: Your solution—reducing drawer width, increasing video height, etc.—is a reasonable compromise. Some drawer text gets clipped, but the core functionality (video viewing + lesson navigation) is preserved. That’s exactly how you handle UX trade-offs.

  5. General mobile app UX principle: The principle you’re following is “reduce unnecessary user effort and context switching.” You want the user to stay in the same device orientation as much as possible while completing the task. That’s a strong UX guideline.

In short: you are on the right track. Mobile web landscape is an edge case that matters for video-heavy apps, and your reasoning is not overcomplicated—it's practical. As long as the layout doesn’t break for desktop, tablet, or Android portrait, your approach is solid.

...


// @ts-ignore only ignores the very next line. It does not affect the whole file.

Example:

// @ts-ignore
label={() => <Text title={lesson.name}>{lesson.name}</Text>} // TypeScript will ignore error here

// This line below will still be checked normally
console.log("This will still show type errors if any");

It’s a line-level directive.

====================

Me: If we use:
label={lesson.name}
(without a function), can we pass some additional prop or whatever to say, give me multiple lines version when needed (no truncation)?

CG: No — if you just use label={lesson.name} in a DrawerItem, you can’t pass additional props like numberOfLines or style, because the label prop in that form only accepts a string (or something coercible to string). That’s why React Navigation (and the underlying react-native-paper/react-native-drawer behavior) always truncates text if it doesn’t fit.

To get multi-line labels, you must provide a function that returns a <Text> element, like you did:

label={() => (
  <Text style={{ /* optional styling */ }}>
    {lesson.name}
  </Text>
)}

This is the only way to pass Text props (multi-line, font size, color, etc.) to the label.

So yes — using the function form is the standard approach when you want multi-line or styled labels.

================

Commit: Improved mobile web landscape UI - menu bar, drawer labels


Me: A strange issue which I think is appearing only after using ScrollView instead of View in lesson screen.
On desktop PC window width increasing beyond 1200 px, the window height increases with the bottom button starting to go off screen and eventually goes off screen. The vertical scrollbar allows user to scroll down to access the bottom button. 
Why is this happening above 1200px? I don't think I have any NativeWind code for such a breakpoint.

[Longish exchange. Eventually some debugging using Chrome DevTools provided styling solution. ...]

Me: ... So I did some web style debugging. In Chrome DevTools for the outer div corresponding to the scroll view, it shows:
<div class="css-view-175oi2r" style="flex-grow: 1;justify-content: space-between;"><video controls="" crossorigin="anonymous" ...
-------------

In DevTools style:
.css-view-175oi2r has 
flex-shrink: 0

I unchecked flex-shrink: 0
After that the problem gets fixed, at least in this round of testing. No matter how small or wide, the width of the window on desktop PC, as the text content is limited, the bottom button is always shown on the screen and at the right place - bottom.

If I check flex-shrink: 0 again, the problem appears again with bottom button going off screen at large desktop window width (> 1200 px). 

CG: Ah — now it makes sense! This is a classic web vs React Native style nuance.

Here’s what’s happening:

  1. ScrollView contentContainerStyle creates a wrapper <div> on web.

  2. flexGrow: 1 is applied, but React Native for Web automatically adds flexShrink: 0 to the inner container.

  3. When flexShrink: 0, the container refuses to shrink when the viewport gets tall/wide — it stretches beyond the visible height if content is short. That’s why your bottom button goes off-screen on large windows.

  4. Unchecking flexShrink: 0 in DevTools allows the container to shrink to fit the viewport, so justifyContent: 'space-between' works correctly and the button stays at the bottom.

Takeaways / fixes:

  • On web, you need flexShrink: 1 (or remove flexShrink: 0) in contentContainerStyle.

  • On native, flexShrink usually doesn’t matter, so this fix is safe.

  • React Native / NativeWind doesn’t expose flexShrink directly in Tailwind classes for web, so you’ll need to add it via style prop.

For example, in your ScrollView:

<ScrollView
  contentContainerStyle={{
    flexGrow: 1,
    flexShrink: 1,  // <= crucial for web to prevent oversizing
    justifyContent: 'space-between',
  }}
  showsVerticalScrollIndicator={true}
  style={{ flex: 1 }}
>
  {/* video + notes + button */}
</ScrollView>

This should consistently fix the large desktop width issue while keeping the bottom button visible when content is short, and still allow scrolling when content is long.

...

Me: That seems to have fixed that problem on web. Notice a minor issue on Android native where the bottom button's bottom part is slightly clipped vertically. So its blue padding below the text of the button is not shown in Android app whereas on web mobile portrait it is shown properly (without vertical scrollbar).

CG: Ah yes — that’s a subtle but common issue. It usually comes from safe area insets on Android (and iOS) not being fully accounted for. On mobile devices, the bottom of the screen may have system navigation bars or insets, so your button can get partially hidden if the padding doesn’t include that.

In your code, you are already using pb-safe in Tailwind (pb-safe → paddingBottom safe area) for the button, ...

...

Me: This works:

          className="bg-primary py-3 flex-row items-center justify-center gap-2"

But I don't know if it is a proper solution.

CG: Yes — that’s actually fine! 🙂

Here’s why it works and why it’s acceptable:

  • py-3 sets equal top & bottom padding, so the text and icon are vertically centered.

  • flex-row items-center justify-center ensures horizontal layout and vertical alignment of the children.

  • gap-2 handles spacing between the text and the icon.

What you were running into before (with py-5 mb-4 pb-safe) was just overlapping/mismatched padding that shifted the content. By using only py-3, you get a clean, balanced layout.

If your only concern is Android clipping a tiny bit at the bottom due to safe area, that’s a minor issue and can be optionally handled with paddingBottom: Platform.OS === 'android' ? 16 : 16 + safeAreaInsetBottom in style.

So yes — for most cases (especially if you’re okay with the tiny Android clipping), your current py-3 flex-row items-center justify-center gap-2 is a proper and simple solution.

...

Commit: Lesson page: Fixed desktop PC wide window bottom button out of view issue; Android bottom button bottom clipping issue

-----------

On mobile web landscape view and also desktop web view for last few lessons, moving to next lesson should, if required, scroll current lesson in open drawer into view.

[CG gave some complex code proposed solution for both Android and web.]

Me: On Android, as I am testing it now only with mobile, I don't have this issue as the Android app is always in Portrait mode and the mobile width is lesser than needed for permanent drawer. So drawer is collapsed and has to opened by user and closed to continue with main lesson page.

I am not testing or worrying about Android tablets whose width may be wide enough for app to show drawer permanently. So we can ignore this condition, as of now at least.

But I need it to be handled on web. So I am very interested in simple solution for web.

[CG provided some code ... some iterations ... After we got it working, I asked CG for a summary note.]

CG: 

Summary: Auto-scrolling active lesson into view in Drawer

🎯 Goal

Ensure that whenever the Drawer is open, the currently active lesson is automatically scrolled into view — particularly useful on web when:

  • the lesson list is long, and

  • the user switches to landscape mode (e.g., mobile web landscape view with permanent drawer).


⚙️ Implementation Steps

  1. Created a ref for the active lesson element

    const activeLessonRef = useRef<HTMLDivElement | null>(null);
  2. Attached the ref only to the active lesson

    label={() => { if (Platform.OS === 'web') { return ( <div ref={isActive ? activeLessonRef : null}> <Text className="dark:text-white">{lesson.name}</Text> </div> ); } else { return ( <View> <Text className="dark:text-white">{lesson.name}</Text> </View> ); } }}
  3. Used useEffect to scroll it into view on changes

    useEffect(() => { if (Platform.OS === 'web' && activeLessonRef.current) { activeLessonRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', }); } }, [pathname, isMobileWebLandscape]);
    • pathname → ensures scrolling occurs when the user navigates to a new lesson.

    • isMobileWebLandscape → ensures scrolling happens again when switching from portrait to landscape mode (where the drawer becomes visible).

    • Using block: 'nearest' avoids excessive movement and keeps UX smooth.


🧩 Behavior

  • On mobile web (portrait): drawer is hidden; no scrolling needed.

  • When switching to landscape (drawer visible): active lesson auto-scrolls into view.

  • When navigating between lessons: active lesson always comes into view smoothly.


Result

A polished, platform-aware drawer experience that:

  • works seamlessly on both web and native,

  • auto-adjusts on orientation change, and

  • maintains user context (current lesson always visible).


...

Commit: Web: moving to next lesson ensures current lesson is shown in open drawer

================================================

Comments