Skip to content

Repository files navigation

Products App

A React Native product catalog built with Expo. Browse products, search, view details, and favorite items — with built-in tools to simulate slow networks and large datasets.


Setup Instructions

Prerequisites

  • Node.js 18+
  • Expo CLI
  • Android Studio (for Android) or Xcode (for iOS)

Windows users: Do not place the project inside a OneDrive synced folder. OneDrive stores files as cloud placeholders (reparse points), which causes the Android Gradle build to fail with not a regular file. Move the project to a local path like C:\Dev\Products.

Installation

# 1. Clone the repo
git clone <your-repo-url>
cd products

# 2. Install dependencies
npm install

# 3. Run on Android
npx expo run:android

Features

  • 2-column product grid with discount badges and star ratings
  • Sorting using price and rating
  • Debounced search (400ms) to avoid unnecessary network requests
  • Skeleton loading grid that matches the real layout while data fetches
  • Animated product detail drawer with image carousel, pricing, and discount info
  • Favorite toggle per card with spring bounce animation
  • Performance Mode — fetches 1000 items to stress-test list rendering
  • Slow 3G Mode — adds a 1.5s delay to simulate poor network conditions
  • Dark/light theme driven by system color scheme

How I Used AI to Build This

I used Claude as a coding assistant throughout this project. I made all the architectural decisions — Claude helped me implement them and pointed out bugs when I described symptoms. Below is an honest breakdown of how that worked.

My Prompting Approach

I asked for the architecture approach and folder layout


1. Splitting into small components

I split the UI into focused, single-responsibility components — ProductCard, ProductRow, ProductDrawer, and CardSkeleton. Each component handles a specific part of the interface, which helps improve rendering performance and loading behavior. This structure also makes it easier to memoize components individually, reduce unnecessary re-renders, and isolate bugs more effectively.

2. Custom theme system

I built a centralized theme with a ThemeContext that reads the system color scheme and exposes a typed theme object to all components. I asked Claude to help wire up the context and the useTheme hook. Every style in the app uses theme tokens (colors, spacing, font sizes, radius) instead of hardcoded values — so dark mode works automatically with zero extra code per component.

3. Shared reusable styles

I created a commonStyles factory that generates shared styles like card, cardBody, and screen from the theme. Components import this instead of redefining the same styles. I asked Claude to help structure the factory pattern so it accepted the theme object as a parameter.

4. Switching from FlatList to VirtualizedList

I chose VirtualizedList over FlatList for explicit control over rendering. FlatList is a wrapper around VirtualizedList that hides the internals — I wanted full control. Since VirtualizedList has no numColumns, I wrote a chunkIntoRows() helper that pairs products into [Product, Product | null] tuples. Claude helped me type it correctly and handle the odd-item edge case.

5. Skeleton loading

I designed the skeleton to mirror the exact structure of the real product grid — two cards per row, same spacing, same dimensions. I asked Claude to help implement the CardSkeleton component and wire it into the loading state. The goal was zero layout shift when real data arrives.

6. Debounced search

I decided early that search should not fire on every keystroke. I asked Claude to implement a useRef-based debounce inside onSearchChange that waits 400ms after the user stops typing before updating debouncedSearch. Only debouncedSearch is in the useEffect dependency array — not the raw search value. This means the network is never hit while the user is still typing.

7. AbortController for request cancellation

I added AbortController to both getProducts and searchProducts so any in-flight request is cancelled when the user changes a switch or types a new search term. Without this, a slow response from a previous request could arrive after a newer one and overwrite the correct state. I also made the slow 3G delay respect the abort signal — if the request is cancelled during the artificial delay, the timeout is cleared immediately rather than hanging for 1.5 seconds.

One issue I ran into: the original code used DOMException to signal abort errors, which doesn't exist in React Native's Hermes JS engine. I replaced it with Object.assign(new Error("Aborted"), { name: "AbortError" }) and updated the catch to use e instanceof Error && e.name === "AbortError".

8. useReducer to fix the skeleton race condition

This was the trickiest bug. The loading skeleton was not showing when I toggled a switch or submitted a search — the old product list stayed visible and then jumped directly to new results. After adding render logs, I found the cause: setLoading(true) and setProducts([]) are two separate state updates. React was batching them in a way that left products populated for one extra render after loading became true, so the condition loading ? skeleton : list kept showing the list.

I replaced both with a single useReducer:

FETCH_START   → { loading: true,  products: [] }   // one render, skeleton guaranteed
FETCH_SUCCESS → { loading: false, products: data } // one render, list appears
FETCH_ERROR   → { loading: false, products: [] }   // one render, empty state

Because the reducer updates both fields atomically, there is no render where loading=true and products still has old data. The skeleton now shows immediately every time.

9. Memoization to fix slow list updates

React Native logged: VirtualizedList: You have a large list that is slow to update. I traced it to renderItem being a new function reference on every render, which broke memoization on ProductCard and caused the entire list to re-render on any state change.

I fixed this in three steps:

  • Wrapped ProductCard in React.memo so it skips re-renders when props are identical
  • Extracted a ProductRow component that creates stable onClick callbacks with useCallback, so memo is never broken by a new function reference
  • Memoized the rows and skeletons arrays with useMemo so they are not recomputed on every render

10. isCancelled flag for unmount safety

On top of AbortController, I added an isCancelled boolean flag in every useEffect. The cleanup function sets it to true before aborting. All dispatch calls inside the async block are guarded by if (!isCancelled). This is a second layer of protection — even if an abort is missed or a response slips through, the dispatch is blocked and no stale state update fires after unmount.


Testing Strategy

I used Android Studio to test the application’s performance, particularly focusing on memory usage, rendering speed, and responsiveness to user interactions. I monitored how the app behaved when loading large datasets, such as when Performance Mode loads 1000+ items, ensuring that scrolling remained smooth without frame drops or UI freezes.

I also tested rapid user interactions, including quickly toggling favorites, typing in the search field, and repeatedly opening and closing the product drawer, to ensure the app handled aggressive input without race conditions or excessive re-renders. Additionally, I tested navigation and backgrounding scenarios by repeatedly opening detail views and sending the app to the background, then returning to verify that the state was preserved and no memory leaks or crashes occurred.

Performance Strategy

VirtualizedList

Only items visible on screen are rendered, plus a small overscan buffer. removeClippedSubviews={true} detaches offscreen items from the native view hierarchy entirely, reducing memory pressure during fast scrolling.

Memoization

React.memo on ProductCard and stable callbacks via useCallback in ProductRow ensure the list only re-renders changed rows, not the entire list on every state update.

Debounced search

At most one fetch fires per 400ms idle period. Without this, every character typed would trigger an abort + new fetch, flood the network, and cause constant flickering.

Atomic state with useReducer

loading and products always transition together in a single render. No intermediate render can show loading=true with stale products or loading=false before products are set.

AbortController + isCancelled

In-flight requests are cancelled immediately when dependencies change. Combined with the isCancelled guard, there are no stale state updates, no dangling async callbacks, and no memory leaks from unmounted components.


Image Optimization

Images are served from dummyjson.com's CDN and loaded with React Native's built-in Image component using resizeMode="contain". The CDN handles compression and resizing server-side.

For a production app I would replace Image with expo-image, which adds:

  • Blurhash placeholder while loading (no blank space while image loads)
  • Automatic memory and disk caching
  • Faster decoding via the native image pipeline

Known Compromises

Favorites reset on reload — Favorite state is local to each ProductCard instance and is not persisted. The fix is to store favorited product IDs in Zustand and persist with zustand/middleware/persist backed by AsyncStorage.

No pagination — The app loads a fixed set of items (20 normally, 1000 in Performance Mode). There is no infinite scroll. The fix is to track a skip offset in state, increment it on onEndReached, and append new pages to the existing product array.

Performance Mode is artificial — Loading 1000 items at once exists only to stress-test the list renderer. Real apps use pagination and never load more than one page at a time.


Project Structure

app/
  _layout.tsx           # Root layout: GestureHandlerRootView + ThemeProvider + Stack
  index.tsx             # Home screen: VirtualizedList, search, toggles, drawer

components/
  home/
    ProductCard.tsx     # Card with image, price, rating, discount badge, favorite toggle
    ProductRow.tsx      # Memoized 2-column row wrapper for VirtualizedList
    ProductDrawer.tsx   # Animated full-screen detail sheet with image carousel
    CardSkeleton.tsx    # Skeleton placeholder matching the card dimensions
    SortControl.tsx     # Sorting component using price and rating with clear function

api/
  productsApi.ts        # getProducts + searchProducts with AbortController and slow3G delay

store/
  performanceStore.ts   # Zustand store: performanceMode and slow3G toggle state

styles/
  common-style.ts       # Shared styles: screen, card, cardBody — themed via factory function

theme/
  ThemeContext.tsx       # ThemeProvider + useTheme hook
  colors.ts             # Color palette for light and dark
  index.ts              # Theme object: colors, spacing, fontSize, radius

utils/
   productsStateReducer.ts   # Handles product state management (loading, error, data)

Troubleshooting Common errors encountered during Android development setup and how to fix them.

  1. OneDrive File Placeholder Error (react-native-screens codegen) Error: Execution failed for task ':react-native-screens:generateCodegenSchemaFromJavaScript'.

    java.io.IOException: Cannot snapshot C:\Users...\node_modules\react-native-screens\src\fabric\bottom-tabs\BottomTabsNativeComponent.ts: not a regular file Cause: The project is inside an OneDrive folder. OneDrive's "Files On-Demand" feature creates placeholder files that aren't real files on disk. Gradle's incremental build system cannot snapshot these placeholders. Fix: Move the project outside of OneDrive to a path like C:\dev: bashxcopy /E /I "C:\Users<you>\OneDrive\Documents\Programming\Products" "C:\dev\Products" cd C:\dev\Products npm install cd android && gradlew clean && cd .. npx react-native run-android

Note: Keep all React Native / Node.js projects outside of OneDrive, Dropbox, or any cloud sync folder. These tools don't handle node_modules well (100k+ small files) and will cause recurring issues.

  1. Android SDK Location Not Found Error: Execution failed for task ':app:processDebugMainManifest'.

    com.android.builder.errors.EvalIssueException: SDK location not found. Define a valid SDK location with an ANDROID_HOME environment variable or by setting the sdk.dir path in your project's local.properties file. Cause: Gradle cannot find the Android SDK because neither local.properties nor the ANDROID_HOME environment variable is configured. Fix: Create or edit android/local.properties and add the path to your SDK: sdk.dir=C:\Users\\AppData\Local\Android\Sdk

Note the escaped backslashes — required in .properties files.

To find your SDK path, open Android Studio → File → Settings → search "Android SDK" and copy the path shown. Alternatively, set ANDROID_HOME as a permanent system environment variable:

Search "Environment Variables" in the Start menu Under User variables, click New

Name: ANDROID_HOME Value: C:\Users<you>\AppData\Local\Android\Sdk

Edit the Path variable and add %ANDROID_HOME%\platform-tools Restart your terminal

  1. AndroidManifest Warnings (usesCleartextTraffic / FileSystemFileProvider) Warnings: application@android:usesCleartextTraffic was tagged at AndroidManifest.xml:6 to replace other declarations but no other declaration present

provider#expo.modules.filesystem.FileSystemFileProvider@android:authorities was tagged at AndroidManifest.xml:0 to replace other declarations but no other declaration present Cause: The debug manifest uses tools:replace to override declarations that don't exist in the main manifest. These are harmless warnings generated by Expo/React Native boilerplate. Fix: These warnings do not affect your build or app behavior and can be safely ignored. If you want to clean them up, open android/app/src/debug/AndroidManifest.xml and remove the tools:replace attributes: xml

<provider android:authorities="${applicationId}.FileSystemFileProvider" ... />

  1. Kotlin Compilation Error — Unresolved Reference 'R' and 'BuildConfig' Error: e: .../MainActivity.kt:18:14 Unresolved reference 'R'. e: .../MainActivity.kt:35:11 Unresolved reference 'BuildConfig'. e: .../MainApplication.kt:32:60 Unresolved reference 'BuildConfig'. Cause: The R and BuildConfig classes are auto-generated during the build. They fail to generate when the namespace is missing from android/app/build.gradle, or when there is a package name mismatch across config files. Fix: Ensure namespace is declared in android/app/build.gradle and matches your package name everywhere: gradleandroid { namespace "com.charlesamiel1.Products" // required compileSdkVersion 34

    defaultConfig {
        applicationId "com.charlesamiel1.Products"
    }
    

    } The package name must be identical in all four places:

android/app/build.gradle → namespace and applicationId android/app/src/main/AndroidManifest.xml → package attribute (if present) MainActivity.kt → package declaration at top of file MainApplication.kt → package declaration at top of file

After fixing, clean and rebuild: bashcd android && gradlew clean && cd .. npx react-native run-android

  1. Java Compilation Error — Package Does Not Exist (Autolinking Mismatch) Error: Execution failed for task ':app:compileDebugJavaWithJavac'.

    error: package com.products does not exist if (com.products.BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { Cause: The autolinking system generates code referencing com.products (derived from the slug in app.json), but the actual Android package is com.charlesamiel1.Products. This happens when app.json is missing an explicit android.package field and Expo falls back to generating one from the slug. Fix: Add the android.package field to app.json: json{ "expo": { "android": { "package": "com.charlesamiel1.Products" }, "ios": { "bundleIdentifier": "com.charlesamiel1.Products" } } } Then regenerate the native Android folder: bash# Back up any custom android changes first xcopy /E /I android android_backup

Regenerate from app.json

npx expo prebuild --clean

Run

npx expo run:android

Warning: --clean will overwrite the entire android folder. Back it up first if you have manual customizations.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages