Repository: vercel/next.js
Pull request: Revert "Remove useSyncExternalStore from useIsDevRendering (#77651)"

This reverts commit f691a326c5fd74813e003cfff4ad02b885b32d08.

f691a326c5fd74813e003cfff4ad02b885b32d08 needs to support React 18 and 19.



Changed files:
filename: packages/next/src/client/components/react-dev-overlay/utils/dev-indicator/dev-render-indicator.tsx
patch: @@ -3,21 +3,35 @@
  * Used by the dev tools indicator to show render status
  */
 
-import { useEffect, useOptimistic } from 'react'
+import { useSyncExternalStore } from 'react'
 
-const optimisticStateSetters = new Set<(boolean: true) => void>()
+let isVisible = false
+let listeners: Array<() => void> = []
+
+const subscribe = (listener: () => void) => {
+  listeners.push(listener)
+  return () => {
+    listeners = listeners.filter((l) => l !== listener)
+  }
+}
+
+const getSnapshot = () => isVisible
+
+const show = () => {
+  isVisible = true
+  listeners.forEach((listener) => listener())
+}
+
+const hide = () => {
+  isVisible = false
+  listeners.forEach((listener) => listener())
+}
 
 export function useIsDevRendering() {
-  const [isDevRendering, setIsDevRendering] = useOptimistic(false)
-  useEffect(() => {
-    optimisticStateSetters.add(setIsDevRendering)
-    return () => {
-      optimisticStateSetters.delete(setIsDevRendering)
-    }
-  }, [setIsDevRendering])
-  return isDevRendering
+  return useSyncExternalStore(subscribe, getSnapshot)
 }
 
-export function setDevRenderIndicatorPending() {
-  optimisticStateSetters.forEach((setter) => setter(true))
+export const devRenderIndicator = {
+  show,
+  hide,
 }

filename: packages/next/src/client/components/react-dev-overlay/utils/dev-indicator/use-sync-dev-render-indicator.tsx
patch: @@ -0,0 +1,16 @@
+import { useEffect, useTransition } from 'react'
+import { devRenderIndicator } from './dev-render-indicator'
+
+export const useSyncDevRenderIndicator = () => {
+  const [isPending, startTransition] = useTransition()
+
+  useEffect(() => {
+    if (isPending) {
+      devRenderIndicator.show()
+    } else {
+      devRenderIndicator.hide()
+    }
+  }, [isPending])
+
+  return startTransition
+}

filename: packages/next/src/client/components/use-action-queue.ts
patch: @@ -1,5 +1,5 @@
 import type { Dispatch } from 'react'
-import React, { startTransition, use } from 'react'
+import React, { use } from 'react'
 import { isThenable } from '../../shared/lib/is-thenable'
 import type { AppRouterActionQueue } from './app-router-instance'
 import type {
@@ -19,20 +19,6 @@ export function dispatchAppRouterAction(action: ReducerActions) {
       'Internal Next.js error: Router action dispatched before initialization.'
     )
   }
-
-  if (process.env.NODE_ENV !== 'production') {
-    // In development, set an pending state on the DevTools indicator
-    // whenever a router action is in progress. This is backed by useOptimistic,
-    // so we don't need to set it back to false; it will be automatically reset
-    // when the transition completes.
-    const setDevRenderIndicatorPending =
-      require('./react-dev-overlay/utils/dev-indicator/dev-render-indicator')
-        .setDevRenderIndicatorPending as typeof import('./react-dev-overlay/utils/dev-indicator/dev-render-indicator').setDevRenderIndicatorPending
-    startTransition(() => setDevRenderIndicatorPending())
-  }
-
-  // NOTE: This is wrapped in a startTransition internally, but to avoid a
-  // refactor hazard we should consider wrapping it here instead.
   dispatch(action)
 }
 
@@ -48,7 +34,22 @@ export function useActionQueue(
   // Ideally, what we'd do instead is pass the state as a prop to root.render;
   // this is conceptually how we're modeling the app router state, despite the
   // weird implementation details.
-  dispatch = (action: ReducerActions) => actionQueue.dispatch(action, setState)
+  if (process.env.NODE_ENV !== 'production') {
+    const useSyncDevRenderIndicator =
+      require('./react-dev-overlay/utils/dev-indicator/use-sync-dev-render-indicator')
+        .useSyncDevRenderIndicator as typeof import('./react-dev-overlay/utils/dev-indicator/use-sync-dev-render-indicator').useSyncDevRenderIndicator
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    const syncDevRenderIndicator = useSyncDevRenderIndicator()
+
+    dispatch = (action: ReducerActions) => {
+      syncDevRenderIndicator(() => {
+        actionQueue.dispatch(action, setState)
+      })
+    }
+  } else {
+    dispatch = (action: ReducerActions) =>
+      actionQueue.dispatch(action, setState)
+  }
 
   return isThenable(state) ? use(state) : state
 }