Skip to main content

8 posts tagged with "Bug Fix"

View all tags

Fix React List Key Duplication Causing DOM Errors

· 2 min read

Encountered this issue while building an AI Agent chat interface. Here's the root cause and solution.

TL;DR

Date.now() millisecond timestamps can duplicate within the same millisecond. When used as React list keys, this causes DOM errors. Fix by adding a random suffix or using crypto.randomUUID().

Problem

When rapidly sending messages in a chat interface, the console shows:

Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

Messages disappear or render incorrectly.

Root Cause

Original code used Date.now() for message IDs:

// ❌ Problematic code
const id = `msg-${Date.now()}-user`

Date.now() returns millisecond timestamps (e.g., 1742345678001). The problem:

  1. Same millisecond = same value — JavaScript's event loop executes synchronous code much faster than 1ms
  2. Rapid operations trigger multiple calls — Fast message sending, SSE streaming creating multiple messages simultaneously
  3. Duplicate keys break reconciliation — React treats same-key elements as identical, causing DOM operation errors

Example: User sends two messages within 1ms, both get key msg-1742345678001-user.

Solution

// ✅ Fixed
const id = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}-user`
  • Math.random().toString(36) generates base-36 random string
  • .slice(2, 9) extracts 7 characters for sufficient uniqueness
  • Timestamp + random string combination has extremely low collision probability

Option 2: Use crypto.randomUUID()

// ✅ More robust (requires modern browser or Node 15.6+)
const id = crypto.randomUUID() // e.g., "550e8400-e29b-41d4-a716-446655440000"
  • Cryptographically secure unique identifier
  • Collision-free guarantee
  • Compatibility: Chrome 92+, Firefox 95+, Safari 15.4+

Option 3: Counter + Timestamp

let counter = 0
const id = `msg-${Date.now()}-${counter++}-user`
  • Simple and reliable
  • Requires maintaining counter state

Complete Example

// Message creation in Zustand store
interface ChatMessage {
id: string
role: 'user' | 'assistant'
content: string
timestamp: number
}

export const useChatStore = create<ChatState>((set) => ({
messages: [],

addUserMessage: (content: string) => {
// ✅ Timestamp + random suffix
const id = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}-user`
const message: ChatMessage = {
id,
role: 'user',
content,
timestamp: Date.now(),
}
set((state) => ({
messages: [...state.messages, message],
}))
return id
},
}))

Key Principles

  1. Keys must be unique and stable — Same element's key cannot duplicate among siblings
  2. Avoid using index as key — Causes issues when list order changes
  3. Avoid timestamp-only keys — Millisecond precision insufficient; microsecond (performance.now()) also unreliable

Interested in similar solutions? Contact us

Debugging Frontend Deployment Not Updating on Production

· 3 min read

TL;DR

Frontend code pushed to Git but new feature missing on production? The root cause is usually outdated build artifacts on the server. Compare local and server dist/ directory timestamps to confirm, then run npm run build on the server.

Problem

New button/feature works locally (npm run dev) but not visible on production:

  • Browser refresh doesn't help
  • Clearing browser cache doesn't help
  • Code logic looks correct
  • Git confirms code was pushed

Root Cause

Frontend static files are typically served directly by Nginx:

Git Push → Server git pull → Server npm run build → Nginx serves dist/

The problem is between step 2 and 3: Code was git pulled, but npm run build wasn't executed or failed.

Common scenarios:

  1. Auto-deploy script syncs code but doesn't trigger build
  2. Manual deploy forgot to run build command
  3. Build ran but output to wrong directory

Solution

Step 1: Compare Build Artifact Timestamps

# Local
ls -la dist/assets/ | head -5

# Server
ssh user@server "ls -la /path/to/project/dist/assets/ | head -5"

Output comparison:

# Local (latest build)
Mar 7 22:55 index-28dFGXhX.js ← contains new feature

# Server (old build)
Mar 7 20:18 index-DsFdnylh.js ← missing new feature

Different filenames (hash changed) means content changed, different timestamps means build not synced.

Step 2: Rebuild on Server

ssh user@server "cd /path/to/project && npm run build"

Step 3: Verify Build Artifacts Updated

ssh user@server "ls -la /path/to/project/dist/assets/"

Confirm timestamps and filenames match local.

Step 4: Refresh Page

Since Vite/Webpack generates new filenames with hashes (e.g., index-28dFGXhX.js), index.html references the new file. Users just need a normal refresh, no forced cache clearing needed.

Complete Debug Script

#!/bin/bash
# Save as check-deploy.sh

SERVER="user@server"
PROJECT_PATH="/path/to/project"

echo "=== Latest Local Commit ==="
git log --oneline -1

echo -e "\n=== Latest Server Commit ==="
ssh $SERVER "cd $PROJECT_PATH && git log --oneline -1"

echo -e "\n=== Local Build Time ==="
ls -la dist/assets/ | head -3

echo -e "\n=== Server Build Time ==="
ssh $SERVER "ls -la $PROJECT_PATH/dist/assets/ | head -3"

echo -e "\n=== Server vs Remote Diff ==="
ssh $SERVER "cd $PROJECT_PATH && git fetch origin && git log HEAD..origin/main --oneline"

FAQ

Q: Why is production still showing old code after Git push?

A: Git push only updates source code. Frontend requires npm run build to generate static files. If your deploy process doesn't automatically trigger a build, the server's dist/ directory remains outdated. Nginx serves static files directly and won't auto-execute builds.

Q: Why doesn't clearing browser cache work?

A: The problem isn't browser cache - the server's static files themselves are old. Even with forced refresh, Nginx returns the old JS/CSS files. The correct fix is updating build artifacts on the server.

Q: How to avoid forgetting to rebuild?

A: Two options: 1) Configure CI/CD for automatic builds (e.g., GitHub Actions); 2) Use git hooks on the server to auto-run npm run build after git pull.

Q: Why does Vite add hash to build filenames?

A: Vite adds content hash to bundle filenames by default (e.g., index-28dFGXhX.js). When content changes, hash changes. This is a cache busting strategy ensuring users always get the latest version while maintaining long-term cache capability.