Notes on learning stage of developing my own React Native education app using Expo Framework - Part 5
- 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
- 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.
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.
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.
-
Aspect ratio alone is insufficient.
-
Combine touch detection + orientation to detect mobile landscape reliably.
-
Both Android and iOS tablets report user-agent strings very similar to phones.
-
Example: An iPad in Safari often has
iPad
or eveniPhone
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.
...
✅ 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
-
Platform.OS === 'web' – correctly limits detection to web builds.
-
width > height – identifies landscape orientation.
-
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.
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 View
→ ScrollView
, 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, needscontentContainerStyle={{ flexGrow: 1 }}
to behave likeView
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.
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
— thediv
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.
-
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. ✅
-
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. ✅
-
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. ✅
-
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.
-
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.
...
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.
================
Here’s what’s happening:
-
ScrollView contentContainerStyle creates a wrapper
<div>
on web. -
flexGrow: 1
is applied, but React Native for Web automatically addsflexShrink: 0
to the inner container. -
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. -
Unchecking
flexShrink: 0
in DevTools allows the container to shrink to fit the viewport, sojustifyContent: 'space-between'
works correctly and the button stays at the bottom.
✅ Takeaways / fixes:
-
On web, you need flexShrink: 1 (or remove
flexShrink: 0
) incontentContainerStyle
. -
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
-----------
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
-
Created a ref for the active lesson element
-
Attached the ref only to the active lesson
-
Used
useEffect
to scroll it into view on changes-
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
Post a Comment