Revisi untuk fix duplicate key warning dan confirm yang stuck.
| # | Issue | Severity |
|---|---|---|
| 1 | Duplicate React key warning setelah redraw | 🔴 Critical |
| 2 | Confirm stuck di "Confirming..." tidak pernah selesai | 🔴 Critical |
┌─────────────────────────────────────────────────────────────────┐
│ UNTUK SETIAP ISSUE: │
│ │
│ 1. ANALISA ROOT CAUSE │
│ - Cari code yang terkait │
│ - Identifikasi mengapa bug terjadi │
│ - Tampilkan code snippet yang bermasalah │
│ │
│ 2. BUAT PLAN │
│ - Files yang akan dimodifikasi │
│ - Perubahan yang akan dilakukan │
│ │
│ 3. TUNGGU APPROVAL dari user │
│ │
│ 4. EKSEKUSI setelah approved │
└─────────────────────────────────────────────────────────────────┘
Warning: Encountered two children with the same key, `mjq0cpv4-7fliokp-2-redraw-0`.
Keys should be unique so that components maintain their identity across updates.
Non-unique keys may cause children to be duplicated and/or omitted — the behavior
is unsupported and could change in a future version.
mjq0cpv4-7fliokp-2-redraw-0
^^^^^^^^ ^^^^^^^ ^ ^^^^^^ ^
| | | | |
| | | | └── redraw index (0)
| | | └────── "redraw" marker
| | └─────────── batch number (2)
| └──────────────── coupon ID atau slot
└───────────────────────── winner ID atau participant ID
- Cari dimana key ini di-generate (kemungkinan di winner card rendering)
- Check apakah redraw create entries dengan key yang sama
- Check apakah old cancelled entry di-delete sebelum create new
- Check bagaimana key di-construct untuk winner cards
Kemungkinan 1: Old winner entry tidak di-delete
// redrawAll() mungkin tidak delete old entry sebelum create new
// Sehingga ada 2 entries dengan slot yang sama
// BUGGY:
const newWinner = await winnerRepository.create({...})
// Old cancelled winner masih ada!
// CORRECT:
await winnerRepository.delete(cancelledWinner.id) // Delete dulu
const newWinner = await winnerRepository.create({...})Kemungkinan 2: Key generation tidak unique setelah redraw
// BUGGY key format:
key={`${winner.participantId}-${winner.batchNumber}-redraw-${index}`}
// Jika redraw berkali-kali, index akan sama
// CORRECT key format - gunakan winner.id yang unique:
key={winner.id}Kemungkinan 3: State tidak di-update dengan benar
// Setelah redraw, state masih hold old + new winners
// Sehingga render duplicate
// BUGGY:
setWinners([...winners, ...newWinners]) // Append, not replace
// CORRECT:
setWinners(prev => {
// Remove old cancelled, add new
const withoutCancelled = prev.filter(w => w.status !== 'cancelled')
return [...withoutCancelled, ...newWinners]
})// 1. Gunakan winner.id sebagai key (selalu unique)
{winners.map(winner => (
<WinnerCard key={winner.id} winner={winner} />
))}
// 2. Pastikan redraw delete old entry
async redrawAll(eventId: string, prizeId: string) {
const cancelledWinners = await winnerRepository.getCancelledUnconfirmed(prizeId)
for (const cancelled of cancelledWinners) {
// DELETE old entry FIRST
await winnerRepository.delete(cancelled.id)
// Then create new
const newWinner = await winnerRepository.create({...})
}
}
// 3. Update state correctly
dispatch({
type: 'REDRAW_COMPLETE',
payload: {
deletedIds: cancelledWinners.map(w => w.id),
newWinners: newWinners
}
})- Klik Confirm → button berubah ke "Confirming..." (disabled)
- Proses tidak pernah selesai
- Tidak ada error di console
- UI tetap stuck
[DrawScreen] handleConfirm called, state: reviewing isConfirming: false
[useDrawState] confirm called
... (tidak ada log lanjutan, tidak ada "confirm succeeded")
- Check
confirmfunction diuseDrawState.ts - Trace flow setelah
confirm called- dimana stuck? - Check apakah ada
awaityang tidak resolve - Check apakah ada error yang tidak ter-catch
- Check apakah
try-catchada tapi tidak log error
Kemungkinan 1: Await yang tidak resolve (promise stuck)
// Ada await yang tidak pernah resolve
const confirm = async () => {
console.log('[useDrawState] confirm called')
// Salah satu await ini stuck:
await winnerRepository.confirmByPrizeId(prizeId) // Stuck?
await couponRepository.voidByParticipantId(...) // Stuck?
await someOtherAsyncOperation() // Stuck?
console.log('[useDrawState] confirm succeeded') // Tidak pernah sampai sini
}Kemungkinan 2: Error tidak ter-catch
// Error terjadi tapi tidak di-catch dengan benar
const confirm = async () => {
console.log('[useDrawState] confirm called')
try {
await riskyOperation() // Throw error
} catch (e) {
// Catch tapi tidak log atau re-throw
// Silent fail, function selesai tanpa log
}
// Tidak sampai sini jika catch tidak continue
console.log('[useDrawState] confirm succeeded')
}Kemungkinan 3: Infinite loop atau deadlock
// Ada loop yang tidak pernah selesai
const confirm = async () => {
console.log('[useDrawState] confirm called')
while (someConditionNeverFalse) {
await something() // Infinite loop
}
console.log('[useDrawState] confirm succeeded')
}Kemungkinan 4: State guard yang salah
// Guard condition prevent execution
const confirm = async () => {
console.log('[useDrawState] confirm called')
// Ini sudah log 'confirm called', tapi masih ada guard setelahnya
if (state.status !== 'reviewing') {
console.warn('[useDrawState] Not reviewing, returning')
return // Return tanpa log 'succeeded'
}
if (hasCancelledWinners) {
console.warn('[useDrawState] Has cancelled, returning')
return // Return tanpa log 'succeeded'
}
// ... rest
}// Tambahkan log DETAIL di setiap step
const confirm = async () => {
console.log('[useDrawState] confirm called')
console.log('[useDrawState] state:', JSON.stringify(state, null, 2))
// Check guards
if (state.status !== 'reviewing') {
console.error('[useDrawState] BLOCKED: status is', state.status)
return
}
if (hasCancelledWinners) {
console.error('[useDrawState] BLOCKED: has cancelled winners')
return
}
console.log('[useDrawState] Starting confirm process...')
try {
console.log('[useDrawState] Step 1: confirmByPrizeId...')
await winnerRepository.confirmByPrizeId(prizeId)
console.log('[useDrawState] Step 1 done')
console.log('[useDrawState] Step 2: voidCouponsForWinners...')
await voidCouponsForWinners()
console.log('[useDrawState] Step 2 done')
console.log('[useDrawState] Step 3: updatePrizeDrawnCount...')
await prizeRepository.updateDrawnCount(prizeId, count)
console.log('[useDrawState] Step 3 done')
console.log('[useDrawState] Step 4: dispatch...')
dispatch({ type: 'CONFIRM_SUCCESS' })
console.log('[useDrawState] Step 4 done')
console.log('[useDrawState] confirm succeeded')
} catch (error) {
console.error('[useDrawState] CONFIRM ERROR:', error)
console.error('[useDrawState] Error stack:', error.stack)
throw error // Re-throw agar bisa di-handle di UI
}
}// winnerRepository.confirmByPrizeId
async confirmByPrizeId(prizeId: string): Promise<void> {
console.log('[WinnerRepo] confirmByPrizeId called:', prizeId)
try {
const result = await db.winners
.where('prizeId')
.equals(prizeId)
.filter(w => w.status === 'valid' && !w.confirmedAt)
.modify({ confirmedAt: new Date().toISOString() })
console.log('[WinnerRepo] confirmByPrizeId done, modified:', result)
} catch (error) {
console.error('[WinnerRepo] confirmByPrizeId ERROR:', error)
throw error
}
}| File | Issue | Changes |
|---|---|---|
src/components/draw/WinnerCard.tsx atau parent |
#1 | Fix key generation - gunakan winner.id |
src/services/drawService.ts |
#1 | Pastikan delete old entry sebelum create new |
src/hooks/useDrawState.ts |
#1, #2 | Fix state update logic, add detailed debug logs |
src/repositories/winnerRepository.ts |
#2 | Add debug logs, check confirmByPrizeId |
src/repositories/couponRepository.ts |
#2 | Add debug logs, check void operations |
1. Issue #2 (Confirm stuck) - BLOCKING, tidak bisa proceed tanpa fix
↓
2. Issue #1 (Duplicate key) - Bisa cause unexpected UI behavior
- No warning di console setelah redraw
- Winner cards tidak duplicate
- Each winner card has unique key
- Setelah redraw, old cancelled tidak tampil duplicate
- Klik Confirm → proses berjalan
- Console log semua steps
- Confirm selesai dalam waktu reasonable (< 5 detik)
- "Confirming..." berubah ke state selanjutnya
- Tidak ada silent error
- winner.confirmedAt ter-set di database
- Semua console.log muncul secara sequential
- Identify step mana yang stuck (jika masih stuck)
- No uncaught errors
- Database operations complete successfully