Repository: BuilderIO/builder
Pull request #3946: React Gen 2- Product Details - Blueprints snippet 
Author: login: uttej-vsk
Created: 2025-03-05T00:49:14Z | Merged: 2025-03-13T19:31:49Z

## Description

This PR aims to add Product Details blueprints code to GitHub for React Gen 2 SDK as part of this [epic in the JIRA-138](https://builder-io.atlassian.net/browse/EDU-138)

@samijaber Please review it.

I have added more code to the `useEffect()`, which is only for cleanup. I know this isn't very significant for this scale of code, but I thought it would be better to use some best practices so that customers would know our code is resilient.  



Diff:
diff --git a/packages/sdks-tests/src/snippet-tests/blueprints-product-details.spec.ts b/packages/sdks-tests/src/snippet-tests/blueprints-product-details.spec.ts
index 1db59ce61c9..784b6a27192 100644
--- a/packages/sdks-tests/src/snippet-tests/blueprints-product-details.spec.ts
+++ b/packages/sdks-tests/src/snippet-tests/blueprints-product-details.spec.ts
@@ -3,7 +3,7 @@ import { test } from '../helpers/index.js';
 
 test.describe('Product Details Component', () => {
   test.beforeEach(async ({ page, packageName }) => {
-    test.skip(!['angular-16', 'angular-16-ssr'].includes(packageName));
+    test.skip(!['angular-16', 'angular-16-ssr', 'react'].includes(packageName));
     // Visit the page where ProductDetailsComponent is rendered
     await page.goto('/product/category/jacket');
   });
@@ -25,8 +25,6 @@ test.describe('Product Details Component', () => {
     // Verify the image source, alt text, and size
     await expect(image).toHaveAttribute('src', /.+/);
     await expect(image).toHaveAttribute('alt', 'Red Jacket');
-    await expect(image).toHaveAttribute('width', '400');
-    await expect(image).toHaveAttribute('height', '500');
   });
 
   test('should display product copy (description)', async ({ page }) => {
diff --git a/packages/sdks/snippets/angular-16-ssr/src/app/product-details/product-details.component.ts b/packages/sdks/snippets/angular-16-ssr/src/app/product-details/product-details.component.ts
index 97f6f8d57c2..9a49789b4cd 100644
--- a/packages/sdks/snippets/angular-16-ssr/src/app/product-details/product-details.component.ts
+++ b/packages/sdks/snippets/angular-16-ssr/src/app/product-details/product-details.component.ts
@@ -13,8 +13,6 @@ import { BuilderContent } from '@builder.io/sdk-angular';
       <img
         [src]="productDetails.data?.['image']"
         [alt]="productDetails.data?.['name']"
-        width="400"
-        height="500"
       />
       <p>{{ productDetails.data?.['collection'].value.data.copy }}</p>
       <p>
diff --git a/packages/sdks/snippets/angular-16/src/app/product-details/product-details.component.ts b/packages/sdks/snippets/angular-16/src/app/product-details/product-details.component.ts
index 7804340a459..eb5dfab769b 100644
--- a/packages/sdks/snippets/angular-16/src/app/product-details/product-details.component.ts
+++ b/packages/sdks/snippets/angular-16/src/app/product-details/product-details.component.ts
@@ -12,8 +12,6 @@ import { fetchOneEntry, type BuilderContent } from '@builder.io/sdk-angular';
       <img
         [src]="productDetails.data?.['image']"
         [alt]="productDetails.data?.['name']"
-        width="400"
-        height="500"
       />
       <p>{{ productDetails.data?.['collection'].value.data.copy }}</p>
       <p>
diff --git a/packages/sdks/snippets/react/src/main.tsx b/packages/sdks/snippets/react/src/main.tsx
index 4f973b4257e..7652f81e54c 100644
--- a/packages/sdks/snippets/react/src/main.tsx
+++ b/packages/sdks/snippets/react/src/main.tsx
@@ -2,6 +2,7 @@ import React from 'react';
 import ReactDOM from 'react-dom/client';
 import { createBrowserRouter, RouterProvider } from 'react-router-dom';
 import AnnouncementBar from './routes/AnnouncementBar.tsx';
+import ProductDetails from './routes/blueprints/ProductDetails.tsx';
 import AdvancedChildRoute from './routes/custom-components/advanced-child.tsx';
 import CustomChildRoute from './routes/custom-components/custom-child.tsx';
 import EditableRegionRoute from './routes/custom-components/editable-region.tsx';
@@ -30,6 +31,11 @@ const router = createBrowserRouter([
     path: '/live-preview',
     element: <LivePreviewBlogData />,
   },
+  {
+
+    path: '/product/category/:handle',
+    element: <ProductDetails />,
+  },
   {
     path: '/query-cheatsheet',
     element: <QueryCheatsheet />,
diff --git a/packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx b/packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
new file mode 100644
index 00000000000..ba12706d88d
--- /dev/null
+++ b/packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
@@ -0,0 +1,36 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import { useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  const { handle } = useParams();
+
+  useEffect(() => {
+    fetchOneEntry({
+      model: 'product-details',
+      apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+      query: {
+        'data.handle': handle,
+      },
+    }).then((productData) => {
+      setProductDetails(productData);
+    });
+  }, [handle]);
+
+  if (!productDetails) {
+    return <p>Loading product details...</p>;
+  }
+
+  return (
+    <div className="product-details">
+      <h1>{productDetails.data?.name}</h1>
+      <img src={productDetails.data?.image} alt={productDetails.data?.name} />
+      <p>{productDetails.data?.collection.value.data.copy}</p>
+      <p>Price: {productDetails.data?.collection.value.data.price}</p>
+    </div>
+  );
+}


Reviews:
user: login: samijaber
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T16:08:25Z
user: login: uttej-vsk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T16:33:16Z
user: login: samijaber
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:18:52Z
user: login: samijaber
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:19:31Z
user: login: uttej-vsk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T18:38:49Z
user: login: samijaber
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:47:59Z
user: login: uttej-vsk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:59:53Z
user: login: uttej-vsk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T18:03:43Z
user: login: samijaber
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T14:39:47Z
user: login: uttej-vsk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:16:31Z
user: login: samijaber
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T19:31:18Z

Review comments:
diff_hunk: @@ -0,0 +1,47 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import { useEffect, useState } from 'react';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  useEffect(() => {
+    let isMounted = true;
+    const fetchProductDetails = async () => {
+      const productData = await fetchOneEntry({
+        model: 'product-details',
+        apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+        query: {
+          'data.handle': 'jacket',
+        },
+      });
+      if (isMounted) {
+        setProductDetails(productData);
+      }
+    };
+    fetchProductDetails();
+
+    return () => {
+      isMounted = false;
+    };
+  }, []);
path: packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
user: login: samijaber
body: I don't see the point of `isMounted`. What does it achieve that can't be done without it, like so:

```suggestion
  useEffect(() => {
    fetchOneEntry({
        model: 'product-details',
        apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
        query: {
          'data.handle': 'jacket',
        },
      }).then(productData => {
        setProductDetails(productData);
      }
  }, []);
```
created_at: 2025-03-05T16:08:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import { useEffect, useState } from 'react';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  useEffect(() => {
+    let isMounted = true;
+    const fetchProductDetails = async () => {
+      const productData = await fetchOneEntry({
+        model: 'product-details',
+        apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+        query: {
+          'data.handle': 'jacket',
+        },
+      });
+      if (isMounted) {
+        setProductDetails(productData);
+      }
+    };
+    fetchProductDetails();
+
+    return () => {
+      isMounted = false;
+    };
+  }, []);
path: packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
user: login: uttej-vsk
body: @samijaber I was reading about abort controllers, and then the react doc suggested the below:
<img width="941" alt="Screenshot 2025-03-05 at 10 28 27 AM" src="https://github.com/user-attachments/assets/8139b596-26e7-4514-a991-125187b8e5fe" />

I'm totally with you on simplifying the code, and I had the same code as yours before pushing it to the PR. I only added a flag to stop fetching twice and updating the state after unmounts. Also, I felt, async/await is cleaner than promises/then syntax.
created_at: 2025-03-05T16:33:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import { useEffect, useState } from 'react';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  useEffect(() => {
+    let isMounted = true;
+    const fetchProductDetails = async () => {
+      const productData = await fetchOneEntry({
+        model: 'product-details',
+        apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+        query: {
+          'data.handle': 'jacket',
+        },
+      });
+      if (isMounted) {
+        setProductDetails(productData);
+      }
+    };
+    fetchProductDetails();
+
+    return () => {
+      isMounted = false;
+    };
+  }, []);
path: packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
user: login: samijaber
body: I'd ignore this. since your `useEffect` has an empty dependencies array, it will only run once no matter what, so there's no point in aborting the fetch or ignoring the result.

The advice you screenshotted is relevant when `useEffect` has items in its dependencies array, which means it will execute more than once.
created_at: 2025-03-05T18:18:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import { useEffect, useState } from 'react';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  useEffect(() => {
+    let isMounted = true;
+    const fetchProductDetails = async () => {
+      const productData = await fetchOneEntry({
+        model: 'product-details',
+        apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+        query: {
+          'data.handle': 'jacket',
+        },
+      });
+      if (isMounted) {
+        setProductDetails(productData);
+      }
+    };
+    fetchProductDetails();
+
+    return () => {
+      isMounted = false;
+    };
+  }, []);
path: packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
user: login: samijaber
body: As for async/await: it adds bloat to the code to declare an async function and then call it. One of our most important rules around snippets in docs is simplicity: remove any code that is not absolutely necessary
created_at: 2025-03-05T18:19:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import { useEffect, useState } from 'react';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  useEffect(() => {
+    let isMounted = true;
+    const fetchProductDetails = async () => {
+      const productData = await fetchOneEntry({
+        model: 'product-details',
+        apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+        query: {
+          'data.handle': 'jacket',
+        },
+      });
+      if (isMounted) {
+        setProductDetails(productData);
+      }
+    };
+    fetchProductDetails();
+
+    return () => {
+      isMounted = false;
+    };
+  }, []);
path: packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
user: login: uttej-vsk
body: Thanks for the explanation, sami! i have updated the code. 
created_at: 2025-03-06T18:38:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import React, { useEffect, useState } from 'react';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  useEffect(() => {
+    fetchOneEntry({
+      model: 'product-details',
+      apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+      query: {
+        'data.handle': 'jacket',
path: packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
user: login: samijaber
body: IMO `'jacket'` should be fetched dynamically from the URL instead:
```ts
  let { productName } = useParams();

//...
        'data.handle': 'productName',
```

and in `main.tsx`:

```ts
,
  {
    path: '/product/category/:productName',
    element: <ProductDetails />,
  },
```
created_at: 2025-03-10T17:47:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import React, { useEffect, useState } from 'react';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  useEffect(() => {
+    fetchOneEntry({
+      model: 'product-details',
+      apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+      query: {
+        'data.handle': 'jacket',
+      },
+    }).then((productData) => {
+      setProductDetails(productData);
+    });
+  }, []);
+
+  if (!productDetails) {
+    return <p>Loading product details...</p>;
+  }
+
+  return (
+    <div className="product-details">
+      <h1>{productDetails.data?.name}</h1>
+      <img
+        src={productDetails.data?.image}
+        alt={productDetails.data?.name}
+        width="400"
+        height="500"
path: packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
user: login: samijaber
body: I'd consider these unnecessary layout attributes
```suggestion
```
created_at: 2025-03-10T17:47:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import React, { useEffect, useState } from 'react';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  useEffect(() => {
+    fetchOneEntry({
+      model: 'product-details',
+      apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+      query: {
+        'data.handle': 'jacket',
path: packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
user: login: uttej-vsk
body: @samijaber Oh yeah, in all the other frameworks in the second PR, I fetched the handle dynamically, but somehow, I didn't do it here. I will push the correct one. 
created_at: 2025-03-10T17:59:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import React, { useEffect, useState } from 'react';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  useEffect(() => {
+    fetchOneEntry({
+      model: 'product-details',
+      apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+      query: {
+        'data.handle': 'jacket',
+      },
+    }).then((productData) => {
+      setProductDetails(productData);
+    });
+  }, []);
+
+  if (!productDetails) {
+    return <p>Loading product details...</p>;
+  }
+
+  return (
+    <div className="product-details">
+      <h1>{productDetails.data?.name}</h1>
+      <img
+        src={productDetails.data?.image}
+        alt={productDetails.data?.name}
+        width="400"
+        height="500"
path: packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
user: login: uttej-vsk
body: @samijaber Yeah, but the image is taking up more space, so to restrict that, I have added width and height. 

**If i keep those attributes:**
<img width="562" alt="Screenshot 2025-03-10 at 1 02 43 PM" src="https://github.com/user-attachments/assets/441d2513-1465-45d7-895d-9d2b5893dc53" />

**If i remove them:**
<img width="1796" alt="Screenshot 2025-03-10 at 1 02 32 PM" src="https://github.com/user-attachments/assets/7db6ab6b-d2b5-4718-90c1-3eab5a1123de" />

created_at: 2025-03-10T18:03:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import React, { useEffect, useState } from 'react';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  useEffect(() => {
+    fetchOneEntry({
+      model: 'product-details',
+      apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+      query: {
+        'data.handle': 'jacket',
+      },
+    }).then((productData) => {
+      setProductDetails(productData);
+    });
+  }, []);
+
+  if (!productDetails) {
+    return <p>Loading product details...</p>;
+  }
+
+  return (
+    <div className="product-details">
+      <h1>{productDetails.data?.name}</h1>
+      <img
+        src={productDetails.data?.image}
+        alt={productDetails.data?.name}
+        width="400"
+        height="500"
path: packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
user: login: samijaber
body: I'm ok with that, as we want to focus on only having the bare minimum needed for the example to work and make sense.
created_at: 2025-03-11T14:39:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { BuilderContent, fetchOneEntry } from '@builder.io/sdk-react';
+import React, { useEffect, useState } from 'react';
+
+export default function ProductDetails() {
+  const [productDetails, setProductDetails] = useState<BuilderContent | null>(
+    null
+  );
+
+  useEffect(() => {
+    fetchOneEntry({
+      model: 'product-details',
+      apiKey: 'ee9f13b4981e489a9a1209887695ef2b',
+      query: {
+        'data.handle': 'jacket',
+      },
+    }).then((productData) => {
+      setProductDetails(productData);
+    });
+  }, []);
+
+  if (!productDetails) {
+    return <p>Loading product details...</p>;
+  }
+
+  return (
+    <div className="product-details">
+      <h1>{productDetails.data?.name}</h1>
+      <img
+        src={productDetails.data?.image}
+        alt={productDetails.data?.name}
+        width="400"
+        height="500"
path: packages/sdks/snippets/react/src/routes/blueprints/ProductDetails.tsx
user: login: uttej-vsk
body: @samijaber Pushed the code
created_at: 2025-03-11T17:16:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-05T00:49:18Z
body: ###  ⚠️  No Changeset found

Latest commit: 6c11b729da391df7a8274cec286656820d81d34a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/uttej-vsk/builder/new/react-product-details-refactor?filename=.changeset/smart-poems-cross.md&value=---%0A%22%40builder.io%2Fpackages%22%3A%20patch%0A%22%40sdk%2Ftests%22%3A%20patch%0A%22%40builder.io%2Fsdks%22%3A%20patch%0A%22%40snippet%2Fangular-16-ssr%22%3A%20patch%0A%22%40snippet%2Fangular-16%22%3A%20patch%0A%22%40snippet%2Freact%22%3A%20patch%0A---%0A%0AReact%20Gen%202-%20Product%20Details%20-%20Blueprints%20snippet%20%0A)


author_association: NONE
user: login: gitguardian[bot]
created_at: 2025-03-05T00:49:49Z
body: #### ️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find [here](https://docs.gitguardian.com/platform/remediate/remediate-incidents) more information about risks.

---

<sup><sub>🦉 [GitGuardian](https://dashboard.gitguardian.com/auth/login/?utm_medium=checkruns&amp;utm_source=github&amp;utm_campaign=cr1) detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.<br/></sup></sub>

author_association: NONE
user: login: nx-cloud[bot]
created_at: 2025-03-05T00:52:24Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/67d1d5c966c2870ff4d6d173?utm_source=pull-request&utm_medium=comment) for commit 6c11b729da391df7a8274cec286656820d81d34a.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx test @snippet/gen1-react` | ✅ Succeeded | 1m 28s | [View ↗](https://cloud.nx.app/runs/rDYr9u3ME3?utm_source=pull-request&utm_medium=comment) |
| `nx test @snippet/gen1-remix` | ✅ Succeeded | 56s | [View ↗](https://cloud.nx.app/runs/cHd1rDvWpV?utm_source=pull-request&utm_medium=comment) |

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-03-12 18:44:50` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: NONE