Skip to content

Feature/UI split - #2

Open
saumya-cw wants to merge 3 commits into
feature/practice-branchfrom
feature/ui-split
Open

saumya-cw wants to merge 3 commits into
feature/practice-branchfrom
feature/ui-split

Conversation

@saumya-cw

Copy link
Copy Markdown
Owner

What This PR Does

Strengthens the app's resilience by introducing an ErrorBoundary for
runtime crash recovery and adding comprehensive edge-case handling across
the data layer and UI. Also migrates to the wger API and refactors the
nutrition display into a multi-product grid.


Error Boundary

New: ErrorBoundary class component
Wraps the entire app. Catches any unhandled runtime errors that bubble up
from the component tree and displays a user-friendly fallback instead of
a blank/broken screen.

Handles:

  • Any component-level JS exception during render
  • Displays the error message for visibility
  • "Try Again" reset button clears error state and re-renders the app
  • Logs error + component info via componentDidCatch for debugging

Edge Case Handling

fetchFood — API layer

  • Unexpected response shape: throws a descriptive error if results is
    missing or not an array
  • Empty result set: returns [] cleanly instead of failing silently
  • Axios errors: extracts HTTP status code for a precise error message
    (e.g. wger API request failed with status 404)
  • Non-Axios errors (e.g. bad response structure): re-thrown as-is so
    ErrorBoundary or useFood can handle them appropriately
  • Incomplete wger entries: filters out items with null energy or empty
    name before mapping, preventing blank cards from rendering

NutritionSection — UI layer

  • Loading state: shows a loading message while fetch is in progress
  • Error state: only shown after loading completes, not simultaneously
  • Empty state: explicit "No products found." message if API returns []
  • Per-card nutriments guard: each card checks product.nutriments before
    rendering NutritionGrid — missing data shows a graceful fallback text
    instead of crashing or rendering an empty card

Other Changes

  • Migrated data source from Open Food Facts to wger Ingredient API
    (free, no API key, returns a list of 12 ingredients)
  • useFood state updated from Product | null to Product[]
  • NutritionSection rebuilt as a responsive 3-column card grid
  • NutritionGrid nutriments prop made required (parent guards before render)
  • Added WgerIngredient and WgerResponse types; removed FoodResponse

Manual Steps for Reviewer

npm run dev
  • App loads and nutrition grid renders correctly
  • Simulate network failure → ErrorBoundary fallback appears with "Try Again"
  • Click "Try Again" → app recovers and re-renders
  • Cards with missing nutriments show fallback text, not a crash

Definition of Done

  • ErrorBoundary catches runtime errors and resets correctly
  • All edge cases in fetchFood handled with specific error messages
  • NutritionSection handles loading, error, empty, and populated states
  • Per-card nutriments guard prevents blank/broken cards

Testing

  • Application loads and renders the nutrition grid
  • No console errors on initial render
  • API data maps and displays correctly across cards
  • Error state renders gracefully without crashing
  • Unit tests pass

saumya-cw added 2 commits February 24, 2026 12:37
- Move useFood() hook from Landing to NutritionSection
- Remove data/loading/error props from component chain
- Simplify FoodHome by removing loading state
- Enable lazy data fetching on user interaction instead of mount
- Improve component independence and testability
- Add ErrorBoundary class component with reset functionality wrapping the app
- Handle edge cases in fetchFood: empty results, unexpected response shape,
  axios vs non-axios errors with HTTP status messages
- Guard per-card missing nutriments in NutritionSection
- Switch data source from Open Food Facts to wger Ingredient API
- Update types (WgerIngredient, WgerResponse) and fetchFood to return Product[]
- Refactor useFood hook state from Product|null to Product[]
- Refactor NutritionSection into responsive 3-column card grid
@mergemitra

mergemitra Bot commented Feb 27, 2026

Copy link
Copy Markdown

Change Summary

Introduces a new App entry wrapped by ErrorBoundary, improving runtime resilience across the experience.
Reimagines the landing flow with FoodHome and a multi-state NutritionSection rendering responsive product cards.
Migrates the data layer to the wger Ingredient API, filtering incomplete items and adding robust error handling.

File Changes
File Summary
src/App.tsx Removes legacy root App component so new src/app entry can take over.
src/app/App.tsx Adds new App entry wrapping FoodLandingPage in ErrorBoundary for resilience.
src/app/ErrorBoundary.tsx Adds ErrorBoundary component to catch runtime errors, log them, and reset view.
src/feature/food/components/FoodHome.tsx Adds FoodHome landing hero that prompts user to start nutrition exploration.
src/feature/food/components/NutritionGrid.tsx Adds NutritionGrid component displaying fixed nutrients grid for given data.
src/feature/food/components/NutritionSection.tsx Adds responsive NutritionSection handling states and rendering product cards grid.
src/feature/food/hooks/useFood.ts Updates useFood hook to return array data state while retaining error/loading.
src/feature/food/pages/Landing.tsx Refactors landing page to toggle FoodHome and NutritionSection views.
src/feature/food/services/food.type.ts Adds WgerIngredient/WgerResponse types and adjusts Product type fields.
src/feature/food/services/foodApi.ts Replaces OpenFoodFacts fetch with wger API, adds robust error handling.
src/main.tsx Updates main entry to load App from new src/app path.

Based on 0afa153...5293b5f

@mergemitra

mergemitra Bot commented Feb 27, 2026

Copy link
Copy Markdown

PR Review

PR Communication Notes

Description Quality

  • ✅ Description clearly explains ErrorBoundary, wger migration, and UI states with reviewer steps
  • ❌ Title isn't conventional commits and is vague; use like 'feat(food): add ErrorBoundary and wger grid'

PR Size & Scope

  • ✅ Size is in the ideal range (272+/144- across 11 files) and stays focused on one feature area
  • ✅ Refactor splits UI into focused components without unrelated churn

Commit Messages

  • ✅ Both commits follow conventional commits ('refactor:' and 'feat:') with clear, detailed bodies
  • ❌ Consider adding scopes in future commits (e.g., 'feat(app): ...', 'refactor(food): ...') for quicker triage

Issue Notes

Code Correctness & Design Quality

  • 🟠 parseFloat can yield NaN for non-numeric strings, which then propagates to the UI; validate with Number.isFinite and map invalid values to undefined/filter the item. (src/feature/food/services/foodApi.ts:35, similar issue exists in src/feature/food/components/NutritionGrid.tsx:21)
  • 🟠 useFood swallows the underlying error and always sets a generic message, losing the detailed status text from fetchFood; capture the error and surface err.message (with a fallback). (src/feature/food/hooks/useFood.ts:15)
  • 🟠 bg-linear-to-br isn’t a standard Tailwind gradient utility (likely meant bg-gradient-to-br), so the background may not render as intended. (src/feature/food/components/NutritionSection.tsx:13)
  • 🟠 Using array index in React keys can cause state/DOM mismatches when the list changes; use a stable unique identifier from the API instead of index. (src/feature/food/components/NutritionSection.tsx:43)

Test Quality & Coverage

  • 🟠 No new/updated automated tests cover the new wger mapping/error handling; regressions may slip (and the PR description should justify missing tests). (src/feature/food/services/foodApi.ts:8, similar issue exists in src/feature/food/hooks/useFood.ts:5, src/feature/food/components/NutritionGrid.tsx:7, src/feature/food/components/NutritionSection.tsx:9, src/app/ErrorBoundary.tsx:13)

Code Readability & Maintainability

  • 🟠 Hardcoding the wger API URL and query params makes env configuration/testing harder and risks breakage if the endpoint changes; move this to env/config constants. (src/feature/food/services/foodApi.ts:6)
💬 Minor Issues (Nitpicks)

Code Correctness & Design Quality

  • 💬 Non-null assertion on the root element bypasses null safety and would crash if the element is missing; handle the null case or document why it’s guaranteed. (src/main.tsx:6)

Code Readability & Maintainability

  • 💬 Generic variable name data reduces clarity (guidelines ban it); rename to a domain term like wgerResponse/response. (src/feature/food/services/foodApi.ts:10, similar issue exists in src/feature/food/hooks/useFood.ts:6, src/feature/food/components/NutritionSection.tsx:10)
  • 💬 Inline arrow functions in JSX props create new callbacks each render and can cause avoidable re-renders; extract handlers (or useCallback if needed). (src/feature/food/pages/Landing.tsx:11, similar issue exists in src/feature/food/pages/Landing.tsx:13)
  • 💬 componentDidCatch uses unknown for errorInfo; prefer React’s ErrorInfo type for better typing and clarity. (src/app/ErrorBoundary.tsx:23)
  • 💬 index.css is imported in multiple entry points, which can cause duplicated side effects and confusion; import global CSS in a single place (usually main.tsx). (src/app/App.tsx:1, similar issue exists in src/main.tsx:3)

Based on 0afa153...5293b5f

Comment thread src/feature/food/services/foodApi.ts Outdated
Comment on lines +31 to +38
.map(({ name, energy, protein, carbohydrates, fat }) => ({
product_name: name,
nutriments: {
"energy-kcal_100g": energy,
proteins_100g: protein ? parseFloat(protein) : undefined,
carbohydrates_100g: carbohydrates ? parseFloat(carbohydrates) : undefined,
fat_100g: fat ? parseFloat(fat) : undefined,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Guard parsed numeric fields so NaN never reaches nutriments (it will render as NaN g); convert non-finite values to undefined.

const toFinite = (v: string | null) => {
  const n = v == null ? NaN : Number.parseFloat(v);
  return Number.isFinite(n) ? n : undefined;
};

const { data, loading, error } = useFood();

return (
<main className="min-h-screen bg-linear-to-br from-emerald-900 via-slate-900 to-black text-white px-6 py-12">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

bg-linear-to-br isn’t a standard Tailwind gradient utility—if you intended a gradient, use bg-gradient-to-br (as in FoodHome) so the background renders.

<main className="min-h-screen bg-gradient-to-br ...">

Comment thread src/feature/food/services/foodApi.ts Outdated
Comment on lines +4 to +6
// wger Ingredient API — free, no API key, dedicated nutrition database
const BASE_URL =
import.meta.env.VITE_MOOD_API_URL ??
"https://jsonplaceholder.typicode.com/users";
"https://wger.de/api/v2/ingredient/?format=json&language=2&page_size=12&ordering=name";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Hardcoding the full wger URL (with magic query params) makes environment config and tests harder—move the base URL to an env var and pass params via axios options.

const BASE_URL = import.meta.env.VITE_WGER_API_URL ?? "https://wger.de/api/v2/ingredient/";

Comment on lines +41 to +44
{data.map((product, index) => (
<div
key={`${product.product_name}-${index}`}
className="bg-slate-800 p-6 rounded-xl shadow-lg hover:shadow-emerald-900/40 hover:scale-[1.01] transition-all"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Avoid index in list keys; use a stable unique id from the API (add id to WgerIngredient/Product during mapping) so React doesn’t mis-associate DOM/state when the list order changes.

<div key={product.id}>

Comment thread src/feature/food/hooks/useFood.ts Outdated
Comment on lines +15 to +16
} catch {
setError("Failed to fetch food data");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Don’t swallow the thrown error from fetchFood—surface its message (with a safe fallback) so the UI can show status-specific failures.

} catch (err) {
  setError(err instanceof Error ? err.message : "Failed to fetch food data");
}

@mergemitra

mergemitra Bot commented Feb 27, 2026

Copy link
Copy Markdown

PR Overview

PR Type: Feature + Refactor

Focus Areas for Architect Review

  • API integration/config: the wger endpoint + query params are hardcoded and the current types don’t expose a stable id; align on env-based configuration + a domain model that supports stable identifiers for list rendering.
  • Data-fetching approach: consider TanStack Query/SWR for retries/caching and to avoid StrictMode double-fetch quirks while enabling a first-class “Retry” UX for network errors.
  • Error handling UX/security: ErrorBoundary renders raw error.message; confirm this is acceptable vs a generic user message + centralized logging/telemetry.
  • Test strategy: add/confirm automated tests for fetchFood edge cases, NutritionSection state branches, and ErrorBoundary reset behavior to prevent regressions.

…ng, and tests

- guard numeric parsing using tofinite to prevent nan values in nutriments
- move wger base url to vite_wger_api_url and pass query params via axios `params`
- add `id` to wgeringredient/product models, map through response, and use `product.id` as react list key
- fix tailwind class typo (`bg-gradient-to-br`) in nutritionsection.tsx
- surface thrown error messages in usefood with safe fallback handling
- rename single-letter variables to descriptive names for improved readability
- update tests, mocks, and test setup to align with implementation changes; all tests passing
@mergemitra

mergemitra Bot commented Mar 3, 2026

Copy link
Copy Markdown

Tip

Need another review?

Tag me and say rereview for re-analysis after you have fixed all the issues.

@mergemitra rereview

@saumya-cw saumya-cw closed this Mar 6, 2026
@saumya-cw
saumya-cw deleted the feature/ui-split branch March 6, 2026 06:59
@saumya-cw
saumya-cw restored the feature/ui-split branch March 6, 2026 07:11
@saumya-cw saumya-cw reopened this Mar 6, 2026
@saumya-cw
saumya-cw requested a review from shashank-CW March 6, 2026 11:49

@shashank-CW shashank-CW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Saumya, here's my in-depth review. You can prioritise fixing the error boundary related items first. Hope you see these as fun puzzles to be solved and not an overwhelming list of items to be done. We can discuss and solve any obstacles you're facing. All the best!

Comment thread src/app/App.tsx
@@ -0,0 +1,12 @@
import "../index.css";
import { FoodLandingPage } from "../feature/food/pages/Landing";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to name "../feature/food/pages/Landing" to "../feature/food/pages/FoodLandingPage"? Why? Why not?

Comment thread src/app/ErrorBoundary.tsx
<div className="min-h-screen flex items-center justify-center bg-black text-white">
<div className="text-center space-y-4">
<h1 className="text-3xl font-bold">⚠️ Something went wrong</h1>
<p className="text-gray-400">Error: {this.state.error?.message}</p>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when message is an empty string?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

current code doesn’t break, but it fails silently in UX when the error message is empty

export function FoodLandingPage() {
const { data, loading, error } = useFood();
const [showNutrition, setShowNutrition] = useState(false);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type View = "home" | "nutrition";
const [view, setView] = useState<View>("home");

Is this better? Why / why not?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Using a union type instead of a boolean makes the UI state explicit,
  • avoids conflicting states, and scales better as more views are added.

Example:
now - "home" | "nutrition"

later can be scaled to - "home" | "nutrition" | "recipe" | "error"

but with boolean:

  • showNutrition
  • showRecipe
  • showError

Comment on lines +11 to +13
<FoodHome onStart={() => setShowNutrition(true)} />
) : (
<div className="w-full max-w-2xl">
<div className="flex justify-center mb-6">
<Button variant="secondary" onClick={() => setShowNutrition(false)}>
Return to Home
</Button>
</div>

{error && (
<p className="text-red-400 text-center mb-4">Error: {error}</p>
)}

{data ? (
<div className="bg-slate-800 p-6 rounded-lg">
<h2 className="text-xl font-semibold mb-4 text-center">
Nutrition Information
</h2>
<h3 className="text-lg font-medium mb-2">{data.product_name}</h3>
{data.brands && (
<p className="text-slate-300 mb-4">Brands: {data.brands}</p>
)}
{data.nutriments && (
<div className="grid grid-cols-2 gap-4">
{[
{
label: "Energy",
value: data.nutriments["energy-kcal_100g"],
unit: "kcal",
},
{
label: "Proteins",
value: data.nutriments.proteins_100g,
unit: "g",
},
{
label: "Fat",
value: data.nutriments.fat_100g,
unit: "g",
},
{
label: "Carbohydrates",
value: data.nutriments.carbohydrates_100g,
unit: "g",
},
].map(({ label, value, unit }) => (
<div key={label}>
<p className="text-sm text-slate-300">{label}</p>
<p className="text-lg">
{value || 0} {unit}
</p>
</div>
))}
</div>
)}
</div>
) : (
<p className="text-slate-300 text-center">No data available.</p>
)}
</div>
<NutritionSection onBack={() => setShowNutrition(false)} />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If https://github.com/saumya-cw/Learning/pull/2/changes#r2969377155 is better, then

<FoodHome onStart={() => setView("nutrition")} />
<NutritionSection onBack={() => setView("home")} />

<div key={label}>
<p className="text-sm text-slate-300">{label}</p>
<p className="text-lg">
{nutriments?.[key] ?? 0} {unit}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const value = nutriments?.[key];

<p className="text-lg">
  {value == null ? "-" : `${value} ${unit}`}
</p>

Is this better?

setData(result);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to fetch food data",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either align the implementation with the PR description or update the PR description. Fetch failures currently render inline error state and do not reach the ErrorBoundary.

import { fetchFood } from "../services/foodApi";
import type { Product } from "../services/food.type";

export const useFood = () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add explicit hook return types

];

(foodApi.fetchFood as any).mockResolvedValue(mockProduct);
(foodApi.fetchFood as any).mockResolvedValue(mockProducts);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid any

});

it("should set error when fetch fails", async () => {
(foodApi.fetchFood as any).mockRejectedValue(new Error("API Error"));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid any

});

it("should have loading true initially", () => {
it("should have loading true initially and empty data", () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passes with a React act(...) warning. The initial-state test mounts the hook but does not control the async update path cleanly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants