Repository: vercel/next.js
Pull request: [dev-overlay] Unify error deduplication logic

[We already dedupe in the shared reducer](https://github.com/vercel/next.js/blob/9db497e821f753c4a4003c5c6c046fd3249b7b57/packages/next/src/client/components/react-dev-overlay/shared.ts#L182-L211) so we don't have to bail out before dispatch in various places.

Changed files:
filename: packages/next/src/client/components/errors/enqueue-client-error.ts
patch: @@ -1,12 +0,0 @@
-// Dedupe the two consecutive errors: If the previous one is same as current one, ignore the current one.
-export function enqueueConsecutiveDedupedError(
-  queue: Array<Error>,
-  error: Error
-) {
-  const previousError = queue[queue.length - 1]
-  // Compare the error stack to dedupe the consecutive errors
-  if (previousError && previousError.stack === error.stack) {
-    return
-  }
-  queue.push(error)
-}

filename: packages/next/src/client/components/errors/use-error-handler.ts
patch: @@ -5,7 +5,6 @@ import { storeHydrationErrorStateFromConsoleArgs } from './hydration-error-info'
 import { formatConsoleArgs, parseConsoleArgs } from '../../lib/console'
 import isError from '../../../lib/is-error'
 import { createConsoleError } from './console-error'
-import { enqueueConsecutiveDedupedError } from './enqueue-client-error'
 import { getReactStitchedError } from '../errors/stitched-error'
 
 const queueMicroTask =
@@ -37,7 +36,7 @@ export function handleConsoleError(
   storeHydrationErrorStateFromConsoleArgs(...consoleErrorArgs)
   attachHydrationErrorState(error)
 
-  enqueueConsecutiveDedupedError(errorQueue, error)
+  errorQueue.push(error)
   for (const handler of errorHandlers) {
     // Delayed the error being passed to React Dev Overlay,
     // avoid the state being synchronously updated in the component.
@@ -60,7 +59,7 @@ export function handleClientError(originError: unknown) {
 
   attachHydrationErrorState(error)
 
-  enqueueConsecutiveDedupedError(errorQueue, error)
+  errorQueue.push(error)
   for (const handler of errorHandlers) {
     // Delayed the error being passed to React Dev Overlay,
     // avoid the state being synchronously updated in the component.

filename: packages/next/src/client/components/react-dev-overlay/ui/container/runtime-error/render-error.tsx
patch: @@ -5,10 +5,6 @@ import type {
 } from '../../../shared'
 
 import { useMemo, useState, useEffect } from 'react'
-import {
-  ACTION_UNHANDLED_ERROR,
-  ACTION_UNHANDLED_REJECTION,
-} from '../../../shared'
 import {
   getErrorByType,
   type ReadyRuntimeError,
@@ -19,17 +15,6 @@ export type SupportedErrorEvent = {
   event: UnhandledErrorAction | UnhandledRejectionAction
 }
 
-function getErrorSignature(ev: SupportedErrorEvent): string {
-  const { event } = ev
-  // eslint-disable-next-line default-case -- TypeScript checks this
-  switch (event.type) {
-    case ACTION_UNHANDLED_ERROR:
-    case ACTION_UNHANDLED_REJECTION: {
-      return `${event.reason.name}::${event.reason.message}::${event.reason.stack}`
-    }
-  }
-}
-
 type Props = {
   children: (params: {
     runtimeErrors: ReadyRuntimeError[]
@@ -72,14 +57,6 @@ const RenderRuntimeError = ({ children, state, isAppDir }: Props) => {
         continue
       }
 
-      // Check for duplicate errors
-      if (idx > 0) {
-        const prev = errors[idx - 1]
-        if (getErrorSignature(prev) === getErrorSignature(e)) {
-          continue
-        }
-      }
-
       next = e
       break
     }
@@ -108,12 +85,7 @@ const RenderRuntimeError = ({ children, state, isAppDir }: Props) => {
     }
   }, [nextError, isAppDir])
 
-  const totalErrorCount = errors.filter((err, idx) => {
-    const prev = errors[idx - 1]
-    // Check for duplicates
-    if (idx > 0) return getErrorSignature(prev) !== getErrorSignature(err)
-    return true
-  }).length
+  const totalErrorCount = errors.length
 
   return children({ runtimeErrors, totalErrorCount })
 }