Skip to content

feat(nutrition): implement favorites state management using zustand - #3

Open
saumya-cw wants to merge 7 commits into
masterfrom
feature/zustand-favorites
Open

saumya-cw wants to merge 7 commits into
masterfrom
feature/zustand-favorites

Conversation

@saumya-cw

Copy link
Copy Markdown
Owner

What does this PR do?

Implements favorites state management for food products using Zustand to ensure consistent UI behavior across product listing and favorites view.

  • Adds Zustand-based useFavoritesStore with persisted favoriteProductIds.
  • Adds a favorite toggle action on each product card using the shared composable Button component.
  • Adds an All Products / Favorites toggle in the nutrition section.
  • Introduces a Favorites-only view that renders only bookmarked products.
  • Extracts reusable, composable UI pieces:
    • FoodCard for product rendering
    • FavoriteToggleButton for favorite/unfavorite action
  • Refactors NutritionSection to reuse FoodCard and reduce duplicated rendering logic.
  • Adds test-safe and SSR-safe persistence fallback storage in the Zustand store.
  • Adds store tests for add/remove/clear favorites behavior.
  • Improves existing hook tests by replacing unsafe any mocks with typed vi.mocked(...).

Manual Testing Steps

  1. Start the app and navigate to the nutrition section.
  2. Verify products are visible under All Products.
  3. Click Favorite on a product card.
  4. Verify the button state changes to Unfavorite.
  5. Switch to Favorites view.
  6. Verify the previously favorited product is visible.
  7. Click Unfavorite on that product from Favorites view.
  8. Verify it is removed from Favorites view.
  9. Favorite multiple products from All Products.
  10. Refresh the page.
  11. Verify favorites persist after reload.
  12. Toggle between All Products and Favorites repeatedly.
  13. Verify state remains consistent and UI updates correctly.

Pull Request Standards Checklist

  • This branch carries a single responsibility (favorites feature implementation).
  • Conventional commit messages and descriptive branch naming have been followed.

Definition of Done

  • The implementation has been validated across multiple scenarios and behaves as expected.
  • Linting and formatting are enabled, and all highlighted issues in modified files are resolved.
  • All non-descriptive comments and dead code have been removed from modified files.
  • The branch has been rebased with the target base branch, and all commits in this PR belong to this implementation.

Testing

  • Manual testing has been performed locally to validate all changes.
  • Automated tests for this implementation have been added/updated and executed locally.

saumya-cw added 7 commits February 13, 2026 15:05
- 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
…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 6, 2026

Copy link
Copy Markdown

Change Summary

Implements Zustand-backed favorites state management across the nutrition experience, persisting bookmarked product IDs and providing toggleable views. Introduces reusable UI pieces (FoodCard, FavoriteToggleButton, NutritionGrid) plus landing, nutrition, and error boundary compositions for consistent navigation between home and favorites views. Refreshes entry/loading configuration and expands testing coverage for hooks, API service, favorites store, and Button component to capture the new behavior.

File Changes
File Summary
PR_MESSAGE.md Adds PR description outlining favorites feature scope, testing steps, and completion checklist.
package.json Adds Zustand dependency and Biome dev tool for favorites implementation.
src/app/App.tsx Introduces App component wiring FoodLandingPage inside ErrorBoundary.
src/app/ErrorBoundary.tsx Adds error boundary component with reset button and styled fallback UI.
src/feature/food/components/FavoriteToggleButton.tsx Adds FavoriteToggleButton using Zustand store to toggle product favorites.
src/feature/food/components/FoodCard.tsx Adds reusable FoodCard showing title, nutrition grid, and favorite button.
src/feature/food/components/FoodHome.tsx Adds FoodHome hero screen with start button to launch nutrition view.
src/feature/food/components/NutritionGrid.tsx Adds NutritionGrid summarizing key nutriment metrics in two-column layout.
src/feature/food/components/NutritionSection.tsx Refactors NutritionSection to toggle views, reuse FoodCard, handle favorites.
src/feature/food/hooks/useFood.test.tsx Adds typed useFood hook tests covering loading, success, and error states.
src/feature/food/hooks/useFood.ts Adds useFood hook fetching products and reporting loading/error states.
src/feature/food/pages/Landing.tsx Adds landing page toggling between FoodHome and NutritionSection.
src/feature/food/services/food.type.ts Adds food type definitions for products, nutriments, and external responses.
src/feature/food/services/foodApi.test.tsx Adds fetchFood tests validating axios interactions and data shaping.
src/feature/food/services/foodApi.ts Adds fetchFood implementation with axios call, validation, and error mapping.
src/feature/food/store/favoritesStore.test.ts Adds favorites store tests ensuring toggle and clear behaviors.
src/feature/food/store/favoritesStore.ts Adds persisted Zustand favorites store with SSR-safe storage fallback.
src/main.tsx Updates entry point to import new App path under app directory.
src/shared/atom/Button.test.tsx Adds Button component tests covering rendering and click behavior.
src/shared/atom/Button.tsx Adds Button atom component with variant styling via clsx utils.
src/shared/atom/button.type.ts Adds ButtonProps type definitions for shared button component.

Based on 8200786...d0a9703

@mergemitra

mergemitra Bot commented Mar 6, 2026

Copy link
Copy Markdown

PR Review

PR Communication Notes

Description Quality

  • ❌ Title/desc focus on favorites but PR also adds ErrorBoundary and wger API fetch/mapping changes.
  • ❌ Description checklist claims single responsibility, but changes span app/, services/, and shared Button tests.

PR Size & Scope

  • ❌ 932 lines across 22 files is manageable, but scope mixes favorites, API migration, ErrorBoundary, and shared UI.
  • ❌ Touches multiple areas (src/app, feature/food/services, shared/atom), suggesting multiple concerns in one PR.

Commit Messages

  • ❌ 2/7 commits are non-conventional: 'implement hooks api with test' and 'implement nutrition dashboard'.
  • ❌ Use '(): ' going forward, e.g., 'test(food): add useFood hook tests'.

Issue Notes

Code Correctness & Design Quality

  • 🟠 The button removes the default focus outline without providing an alternative focus-visible style, making keyboard focus hard/impossible to see. (src/shared/atom/Button.tsx:5)
  • 🟠 The async useEffect has no cleanup/abort, so an in-flight request can still call setState after unmount (memory leak / React warning) and wastes network work. (src/feature/food/hooks/useFood.ts:10)
  • 🟠 toFinite() returns undefined but its result is assigned to optional number fields, which breaks exactOptionalPropertyTypes semantics and can leak undefined values; omit keys when parsing fails or widen the type explicitly. (src/feature/food/services/foodApi.ts:50)
  • 🟠 The favorite/unfavorite control is a toggle but doesn’t set aria-pressed, so assistive tech won’t reliably announce the current state. (src/feature/food/components/FavoriteToggleButton.tsx:15, similar issue exists in src/feature/food/components/NutritionSection.tsx:38)
  • 🟠 Missing nutriment values are rendered as 0, which can mislead users (unknown ≠ zero); render “N/A”/“—” or omit the value when data is missing. (src/feature/food/components/NutritionGrid.tsx:21)

Test Quality & Coverage

  • 🟠 Tests use the shared userEvent instance instead of userEvent.setup() per test/beforeEach, which can leak interaction state across tests and cause flakiness. (src/shared/atom/Button.test.tsx:34)
  • 🟠 Store tests reset in-memory state but don’t clear persisted storage, so localStorage rehydration can leak state between runs and make tests flaky. (src/feature/food/store/favoritesStore.test.ts:6)
  • 🟠 The “initial state” test doesn’t control the mocked fetchFood promise, so the effect may resolve and update state during/after the assertion (act warnings / flakiness); mock a pending promise or unmount/await deterministically. (src/feature/food/hooks/useFood.test.tsx:13)
  • 🟠 Tests don’t cover the axios.isAxiosError branches (status vs network) or invalid response-shape errors, so regressions in error handling/validation won’t be caught. (src/feature/food/services/foodApi.test.tsx:58)
  • 🟠 Favorites behavior is wired through UI state + store but there are no component/integration tests verifying toggling favorites updates both views correctly (easy regression point). (src/feature/food/components/NutritionSection.tsx:11)

Code Readability & Maintainability

  • 🟠 New dependencies were added but no lockfile change is included, which can break reproducible installs and CI. (package.json:20)
  • 🟠 Button props are too restrictive (no pass-through native button/aria props and onClick typed as () => void), which limits reuse and blocks accessibility attributes like aria-pressed/aria-label. (src/shared/atom/button.type.ts:7, similar issue exists in src/shared/atom/Button.tsx:22)
  • 🟠 Using the generic name data makes the hook’s intent unclear and violates the project’s banned vague names; prefer products/foodProducts. (src/feature/food/hooks/useFood.ts:6, similar issue exists in src/feature/food/services/foodApi.ts:10, src/feature/food/components/NutritionSection.tsx:12)
  • 🟠 Using as any in tests bypasses type checking and can hide broken mocks; prefer vi.mocked(...) / typed mocks so the test matches the real axios API. (src/feature/food/services/foodApi.test.tsx:13)
💬 Minor Issues (Nitpicks)

Code Readability & Maintainability

  • 💬 VARIANT_CLASSES is typed as Record<string, string> so TypeScript won’t catch missing/extra variants; type it as Record<ButtonType, string> (or as const satisfies). (src/shared/atom/Button.tsx:7)
  • 💬 API params like language: 2 and page_size: 12 are magic numbers; extract to named constants for readability and easier tuning. (src/feature/food/services/foodApi.ts:13)
  • 💬 Inline onClick={() => ...} creates a new function each render; prefer a named handler (optionally useCallback) if this button is rendered in large lists or memoized parents. (src/feature/food/components/FavoriteToggleButton.tsx:17, similar issue exists in src/feature/food/components/NutritionSection.tsx:40, src/feature/food/pages/Landing.tsx:11)
  • 💬 The fields array is recreated on every render; consider hoisting it outside the component since it’s static. (src/feature/food/components/NutritionGrid.tsx:8)
  • 💬 index.css is imported here and also in main.tsx; double-importing global CSS can cause redundancy or ordering surprises—prefer a single import location. (src/app/App.tsx:1)
  • 💬 componentDidCatch uses errorInfo: unknown; prefer React’s ErrorInfo type so logging/telemetry stays typed and future-safe. (src/app/ErrorBoundary.tsx:23)

Based on 8200786...d0a9703

Comment on lines +22 to +26
<button
type={type}
disabled={disabled}
onClick={onClick}
className={clsx(BASE_CLASS, VARIANT_CLASSES[variant])}

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

Button should be composable and accessible: extend native button props (incl. aria-*/data-*/className), forward ...rest to the <button>, and add a visible focus-visible style instead of removing outlines.

export function Button({ variant = "primary", className, ...rest }: ButtonProps) {
  return (
    <button {...rest} className={clsx(BASE_CLASS, VARIANT_CLASSES[variant], className)} />
  );
}

Comment on lines +15 to +19
<Button
variant={isFavorite ? "secondary" : "primary"}
onClick={() => toggleFavorite(productId)}
>
{isFavorite ? "Unfavorite" : "Favorite"}

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

Expose the favorite state to assistive tech by setting aria-pressed on this toggle button (requires Button to forward native props).

<Button
  aria-pressed={isFavorite}
  variant={isFavorite ? "secondary" : "primary"}
  onClick={() => toggleFavorite(productId)}
>

Comment on lines +20 to +22
<p className="text-lg">
{nutriments?.[key] ?? 0} {unit}
</p>

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 render missing nutriments as 0 (unknown ≠ zero); display an explicit placeholder like “—/N\u002fA”, and consider omitting undefined keys when mapping API data to align with strict optional-property semantics.

const value = nutriments[key];
<p className="text-lg">{value == null ? "—" : `${value} ${unit}`}</p>

Comment on lines +10 to +14
useEffect(() => {
const getFood = async () => {
try {
const result = await fetchFood();
setData(result);

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

Add an abort/cleanup path for the async effect to prevent setState on unmounted components; pass the AbortSignal down to fetchFood (axios supports { signal }).

useEffect(() => {
  const controller = new AbortController();
  ...
  return () => controller.abort();
}, []);

onBack: () => void;
};

export function NutritionSection({ onBack }: Props) {

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

Add a component/integration test for favorites wiring (favorite/unfavorite + All Products/Favorites switching) by mocking useFood() to return deterministic products, using userEvent.setup(), and resetting/clearing the zustand persist state between tests.

@mergemitra

mergemitra Bot commented Mar 6, 2026

Copy link
Copy Markdown

PR Overview

PR Type: Feature

Focus Areas for Architect Review

  • Addition of new dependencies (zustand, @biomejs/biome) — confirm alignment with the project’s standard state management and formatting/linting approach.
  • Favorites persistence stores only product IDs while the UI renders from a paged API response; confirm the intended UX when persisted favorites are not present in the currently fetched product set.
  • 3rd‑party API typing/validation is currently manual; if this surface grows, consider standardizing runtime validation at the boundary (schema-based) for long-term safety/consistency.

@saumya-cw
saumya-cw requested a review from shashank-CW March 6, 2026 11:49
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.

1 participant