Why useEffect runs twice in Next.js dev
Technology

Why useEffect runs twice in Next.js dev

If your logs, API calls, or subscriptions fire twice in local dev, you are probably seeing React's development-only Strict Mode check. Here is what to fix and what not to panic about.

2026-06-06
6 min read
Why useEffect runs twice in Next.js dev

Why useEffect runs twice in Next.js dev#

You add a console.log, start next dev, and the effect fires twice. Sometimes that means two fetches, two analytics pings, or two subscriptions.

The root cause is usually not Next.js. React documents this behavior directly: when Strict Mode is on, React runs one extra development-only setup and cleanup cycle before the real setup.

That means the question is not "how do I stop React from doing that?" The question is "why does my effect break when React stress-tests it?"

The broken effect pattern#

This is the classic example:

tsx
'use client'

import { useEffect, useState } from 'react'

export default function UserList() {
  const [users, setUsers] = useState<string[]>([])

  useEffect(() => {
    fetch('/api/users')
      .then((res) => res.json())
      .then((data) => setUsers(data))
  }, [])

  return <pre>{JSON.stringify(users, null, 2)}</pre>
}

In development, you may see two requests. The effect has no cleanup and no cancellation, so the extra Strict Mode cycle reveals that weakness immediately.

A safer effect#

tsx
'use client'

import { useEffect, useState } from 'react'

export default function UserList() {
  const [users, setUsers] = useState<string[]>([])

  useEffect(() => {
    const controller = new AbortController()

    async function loadUsers() {
      const res = await fetch('/api/users', { signal: controller.signal })
      const data = await res.json()
      setUsers(data)
    }

    loadUsers().catch((error) => {
      if (error.name !== 'AbortError') throw error
    })

    return () => controller.abort()
  }, [])

  return <pre>{JSON.stringify(users, null, 2)}</pre>
}

Now the cleanup mirrors the setup. That is exactly what the React docs want Strict Mode to test.

What React is actually telling you#

If a second setup+cleanup cycle causes visible breakage, your effect is probably doing one of these:

  • opening a subscription and never unsubscribing
  • firing a network request with no cancellation
  • mutating global state during setup
  • using an effect for something that should happen in response to a user event instead

The better fix in many Next.js apps: move the fetch to the server#

In App Router projects, a lot of client useEffect fetching should not be client fetching at all.

If the data is needed to render the page, fetch it in the Server Component page or loader path first. That removes the whole class of duplicate dev-fetch confusion.

tsx
// app/users/page.tsx
export default async function Page() {
  const res = await fetch('https://example.com/api/users', {
    cache: 'no-store',
  })
  const users = await res.json()

  return <pre>{JSON.stringify(users, null, 2)}</pre>
}

That is not always the right move, but it is often the right default in modern Next.js.

What not to do first#

Do not start by disabling Strict Mode just because the effect is noisy. If the extra cycle exposes a bug, production can still hit the same bug through remounts, navigation, interrupted renders, or subscription leaks.

Use Strict Mode as the warning sign it is meant to be.

When a useRef guard is acceptable#

For genuinely one-off client actions that cannot be moved elsewhere, a guard can be acceptable:

tsx
'use client'

import { useEffect, useRef } from 'react'

export default function ClientPing() {
  const sent = useRef(false)

  useEffect(() => {
    if (sent.current) return
    sent.current = true
    void fetch('/api/ping', { method: 'POST' })
  }, [])

  return null
}

This is a tactical fix, not the first tool to reach for. Prefer clean setup/cleanup or server-side data flow when possible.

For related React and App Router debugging:

References#

Frequently Asked Questions

|

Have more questions? Contact us

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.