A React Native app for a small independent bookstore. Customers can browse the catalog, view book details with reviews, manage a shopping cart, and check out. Built for the Bumpa Mobile Engineer assessment.
Stack: React Native 0.86 (New Architecture, Hermes), React 19, TypeScript, React Navigation 7, Zustand, Reanimated 4, MMKV.
- Catalog — paginated, infinite-scroll list with pull-to-refresh, shimmer skeleton loading, lazy-loaded cover images, and an "in cart" indicator on books already added.
- Search — debounced, full-catalog search by title, author, or genre from a persistent search bar, with its own loading and "no matches" states.
- Book details — async data fetch with loading/error/empty states, a natively-blurred cover backdrop with parallax, aggregate rating from reviews, and a pinned add-to-cart bar with a fly-to-cart animation.
- Cart — add/remove items, per-item quantity steppers, and a live total. Persisted across restarts.
- Checkout — order summary, a simulated payment request with a loading state, and a spring-up success sheet that clears the cart on dismiss.
- Light & dark mode — follows the system by default with an in-app toggle.
- Splash & branding — a static launch splash (logo + wordmark) that matches the native launch screen; the app ships as "Book Note".
- Buttery animations — staggered entrances, spring press feedback, scroll-driven parallax, and the fly-to-cart flourish, all on the UI thread via Reanimated.
The cart is global, read by many screens (list badge, details bar, cart,
checkout) and must survive app restarts. Zustand gives a small, hook-based
store with selector subscriptions (useCartTotal, useCartCount,
useIsInCart) so a quantity change re-renders only what depends on it — not
every card. Persistence is wired through zustand/middleware persist backed
by MMKV. Context was considered but would re-render all consumers on any cart
change and needs extra plumbing for persistence.
Data fetching lives in useEffect-based hooks (usePaginatedBooks,
useBookDetails, useBookSearch) that own the loading / error / empty /
success states. Cleanup guards keep the UI safe across unmounts and races: a
cancelled flag makes a late response a no-op (rather than setting state on an
unmounted component), and search tags each request so a slow earlier response
can't overwrite a newer one. Screens stay declarative and just render state.
src/api wraps a mock client with an MMKV-backed cache and a NetInfo-aware
cacheThenNetwork helper: it serves cache immediately when offline, falls back
to cache on request failure, and only surfaces an error when there is genuinely
nothing to show. This keeps the cart and previously-viewed data usable without
a connection.
All motion runs on the UI thread: layout entrances (FadeInDown, SlideInDown),
a reusable spring PressableScale, a shimmer Skeleton sweep, and the details
hero's scroll-driven parallax via useAnimatedScrollHandler. Shared springs and
durations live in theme/tokens.ts (motion) so the feel is consistent.
ThemeProvider resolves the active palette from the OS scheme with a persisted
manual override (light / dark / system). Components read colors via useTheme()
and build styles with useThemedStyles(makeStyles), so a theme switch restyles
the whole app — including React Navigation headers, which get a matching theme
from useNavigationTheme(). Palettes live in theme/palettes.ts with identical
keys enforced by the ThemeColors type.
The details hero uses React Native's core <Image blurRadius> for a genuine
native blur of the book's own cover — no extra native dependency, so it runs
after a plain npm install with no linking step. A themed scrim and a
three-band fade blend it into the page, with the crisp cover floating above.
- Pagination:
FlatListwithonEndReachedfetches 10 books per page. - Pagination race guard:
onEndReachedfires repeatedly while a state-based flag updates asynchronously, so a synchronoususeRefguards against concurrent fetches and appends de-dupe by id — no duplicate pages/books. - Debounced search: 300ms debounce plus per-request tagging so only the latest query's results are applied.
- Lazy images:
LazyImageshows a pulse placeholder and a graceful fallback, loading covers on demand. - List tuning:
initialNumToRender, keyed items, item separators. - MMKV for synchronous, fast reads of the cart and API cache on every mount.
- Selector subscriptions to minimize re-renders.
src/
api/ Mock client, types, cache, NetInfo-aware fetch layer
assets/ Splash logo
components/ Shared UI (common/, book/, cart/) incl. SplashScreen, SearchBar
navigation/ Native-stack navigator + themed navigation theme
screens/ BookList, BookDetails, Cart, Checkout (each with local
components/ and hooks/)
store/ Zustand cart store + MMKV persistence
theme/ Palettes, tokens, ThemeProvider/useTheme/useThemedStyles
utils/ Formatting + MMKV instance
Complete the RN Set Up Your Environment guide first.
Install JS dependencies:
npm installStart Metro:
npm startFirst clone, or after changing native deps:
bundle install # installs CocoaPods via Bundler (first time only)
bundle exec pod installRun:
npm run iosnpm run androidnpm testJest + React Native Testing Library. Native modules (Reanimated, MMKV, NetInfo,
Safe Area) are mocked in jest.setup.js; jest.config.js extends the RN preset
and adds transformIgnorePatterns for React Navigation's ESM.
Coverage includes the assessment's required areas:
| Area | Test |
|---|---|
| Book price component | __tests__/BookPrice.test.tsx |
| Cart add/remove/quantity/total | __tests__/cartStore.test.ts |
| Cart item add/remove UI | __tests__/CartItem.test.tsx |
| In-cart indicator on cards | __tests__/BookCard.test.tsx |
| Books API (fetch, pagination, offline, search) | __tests__/booksApi.test.ts |
| Pagination guard (no duplicate pages) | __tests__/usePaginatedBooks.test.ts |
| Debounced catalog search | __tests__/useBookSearch.test.ts |
| Checkout flow (order → success → clear) | __tests__/CheckoutScreen.test.tsx |
| Price formatting util | __tests__/formatPrice.test.ts |
| App smoke test | __tests__/App.test.tsx |
Note: React Native Testing Library v14 renders asynchronously — tests
await render(...)/await fireEvent.press(...)accordingly.
- The API is mocked (
src/api/mock) with a simulated ~500ms network delay; there is no real backend or payment gateway. "Place Order" simulates a request and then clears the persisted cart. The delay is skipped under Jest for deterministic, leak-free tests. - Prices are displayed in NGN by default via
formatPrice. - Cover images use deterministic placeholder art seeded by book id.
- Native splash / app name are configured natively (Android via the
AndroidX
core-splashscreenAPI; iOS viaLaunchScreen). These require a full rebuild (npm run android/npm run ios) — they don't hot-reload. - Commands below use
npm;yarnworks too (ayarn.lockis committed).