fix: replace index-based React keys with stable data keys (F-9)

Use page paths, referrer URLs, item names, and composite location
keys instead of array indices. Prevents stale-row glitches when
lists are filtered or reordered.
This commit is contained in:
Usman Baig
2026-03-01 21:15:09 +01:00
parent 501932849b
commit fd1386b80d
10 changed files with 26 additions and 23 deletions

View File

@@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Improved
- **More reliable list rendering across the dashboard.** Pages, referrers, locations, devices, funnels, and other data lists now use stable identifiers (like page paths and referrer URLs) instead of array positions as their React keys. This prevents potential display glitches — such as stale data appearing in the wrong row — when lists are filtered, sorted, or updated in real time.
- **Deleting a site now fully cleans up all its data.** Previously, deleting a site removed the site record but could leave behind orphaned analytics events in the database, slowly accumulating unused data. Now all events are cleaned up automatically in small batches before the site is removed, keeping your database tidy.
- **Modern config loading for the app.** The app's configuration file now uses proper ES module imports instead of an older CommonJS pattern. This aligns with modern JavaScript standards and enables better tooling support.
- **Complete API documentation in the backend README.** The developer documentation now lists all 80+ endpoints organized by category — ingestion, public dashboard, sites, analytics, real-time, goals, funnels, uptime, billing, notifications, admin, and audit. Previously only 12 endpoints were documented, which understated the full scope of the system.
- **Login page now shows a loading screen while redirecting.** Previously, the login page briefly showed a blank white screen before redirecting you to the sign-in page. You'll now see the Pulse logo and a "Redirecting to log in..." message, matching the signup page experience.

View File

@@ -114,7 +114,7 @@ export default function FAQPage() {
<div className="max-w-3xl mx-auto">
{faqs.map((faq, index) => (
<FAQItem key={index} faq={faq} index={index} />
<FAQItem key={faq.question} faq={faq} index={index} />
))}
</div>

View File

@@ -78,8 +78,8 @@ function ComparisonSection() {
{ feature: "GDPR Compliant", pulse: true, ga: "Complex" },
{ feature: "Script Size", pulse: "< 1 KB", ga: "45 KB+" },
{ feature: "Data Ownership", pulse: "Yours", ga: "Google's" },
].map((row, i) => (
<tr key={i} className="hover:bg-neutral-50/50 dark:hover:bg-neutral-800/50 transition-colors">
].map((row) => (
<tr key={row.feature} className="hover:bg-neutral-50/50 dark:hover:bg-neutral-800/50 transition-colors">
<td className="p-6 text-neutral-900 dark:text-white font-medium">{row.feature}</td>
<td className="p-6">
{row.pulse === true ? (
@@ -303,7 +303,7 @@ export default function HomePage() {
{ icon: ZapIcon, title: "Lightweight", desc: "Our script is less than 1kb. It won't slow down your site or affect your SEO." }
].map((feature, i) => (
<motion.div
key={i}
key={feature.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}

View File

@@ -278,7 +278,7 @@ export default function FunnelReportPage() {
</thead>
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-800">
{stats.steps.map((step, i) => (
<tr key={i} className="hover:bg-neutral-50 dark:hover:bg-neutral-800/30 transition-colors">
<tr key={step.step.name} className="hover:bg-neutral-50 dark:hover:bg-neutral-800/30 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<span className="w-6 h-6 rounded-full bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-xs font-medium text-neutral-600 dark:text-neutral-400">

View File

@@ -149,7 +149,7 @@ export default function CreateFunnelPage() {
</div>
{steps.map((step, index) => (
<div key={index} className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-4">
<div key={`step-${index}`} className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-4">
<div className="flex items-start gap-4">
<div className="mt-3 text-neutral-400">
<div className="w-6 h-6 rounded-full bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-sm font-medium text-neutral-600 dark:text-neutral-400">

View File

@@ -117,7 +117,7 @@ export default function FunnelsPage() {
)}
<div className="flex items-center gap-2 mt-4">
{funnel.steps.map((step, i) => (
<div key={i} className="flex items-center text-sm text-neutral-500">
<div key={step.name} className="flex items-center text-sm text-neutral-500">
<span className="px-2 py-1 bg-neutral-100 dark:bg-neutral-800 rounded-lg text-neutral-700 dark:text-neutral-300">
{step.name}
</span>

View File

@@ -132,8 +132,8 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
</div>
) : hasData ? (
<>
{displayedData.map((page, index) => (
<div key={index} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
{displayedData.map((page) => (
<div key={page.path} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center">
<a
href={`https://${domain.replace(/^https?:\/\//, '')}${page.path}`}
@@ -181,8 +181,8 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
<ListSkeleton rows={10} />
</div>
) : (
(fullData.length > 0 ? fullData : data).map((page, index) => (
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
(fullData.length > 0 ? fullData : data).map((page) => (
<div key={page.path} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center">
<a
href={`https://${domain.replace(/^https?:\/\//, '')}${page.path}`}

View File

@@ -247,8 +247,8 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
) : (
hasData ? (
<>
{displayedData.map((item, index) => (
<div key={index} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
{displayedData.map((item) => (
<div key={`${item.country ?? ''}-${item.region ?? ''}-${item.city ?? ''}`} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
{activeTab === 'countries' && <span className="shrink-0">{getFlagComponent(item.country ?? '')}</span>}
{activeTab !== 'countries' && <span className="shrink-0">{getFlagComponent(item.country ?? '')}</span>}
@@ -296,8 +296,8 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
<ListSkeleton rows={10} />
</div>
) : (
(fullData.length > 0 ? fullData : data).map((item, index) => (
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
(fullData.length > 0 ? fullData : data).map((item) => (
<div key={`${item.country ?? ''}-${item.region ?? ''}-${item.city ?? ''}`} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
<span className="shrink-0">{getFlagComponent(item.country ?? '')}</span>
<span className="truncate">

View File

@@ -156,8 +156,8 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
</div>
) : hasData ? (
<>
{displayedData.map((item, index) => (
<div key={index} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
{displayedData.map((item) => (
<div key={item.name} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
{item.icon && <span className="text-lg">{item.icon}</span>}
<span className="truncate">{item.name}</span>
@@ -198,8 +198,8 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
<ListSkeleton rows={10} />
</div>
) : (
(fullData.length > 0 ? fullData : data).map((item, index) => (
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
(fullData.length > 0 ? fullData : data).map((item) => (
<div key={item.name} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
{item.icon && <span className="text-lg">{item.icon}</span>}
<span className="truncate">{item.name === 'Unknown' ? 'Unknown' : item.name}</span>

View File

@@ -102,8 +102,8 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
</div>
) : hasData ? (
<>
{displayedReferrers.map((ref, index) => (
<div key={index} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
{displayedReferrers.map((ref) => (
<div key={ref.referrer} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
{renderReferrerIcon(ref.referrer)}
<span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span>
@@ -144,8 +144,8 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
<ListSkeleton rows={10} />
</div>
) : (
mergeReferrersByDisplayName(fullData.length > 0 ? fullData : filteredReferrers).map((ref, index) => (
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
mergeReferrersByDisplayName(fullData.length > 0 ? fullData : filteredReferrers).map((ref) => (
<div key={ref.referrer} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
{renderReferrerIcon(ref.referrer)}
<span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span>