---
title: Activity with Cache Components
nav_title: Activity with Cache Components
description: Learn how to handle UI patterns like dropdowns, dialogs, and forms that behave differently when Cache Components keeps routes mounted with Activity.
related:
  title: Related
  description: Learn more about Activity and Cache Components.
  links:
    - app/getting-started/cache-components
    - app/guides/activity
---

When [Cache Components](/docs/app/getting-started/cache-components) is enabled, routes don't unmount when you navigate away from them. Instead, Next.js uses React's [`<Activity>`](https://react.dev/reference/react/Activity) component to hide them - setting them to `mode="hidden"` rather than removing them from the tree.

This preserves component state and DOM state, which is great for back/forward navigation. But it can also surprise you: patterns that relied on components unmounting to reset state will now behave differently.

Next.js uses a **3-entry limit** - it preserves up to 3 routes. When you navigate beyond that, the oldest route's DOM and React state are dropped. This means if a user navigates through many pages and then goes back, very old states may have been evicted and will re-render fresh.

### Why Activity ships with Cache Components

Cache Components and Activity work together to enable SPA-like navigation - instant route transitions without sacrificing server rendering:

- **Server Components** use `"use cache"` to extend their lifetime, enabling prefetching and instant route transitions
- **Client Components** use Activity to preserve their state and DOM across navigations - without it, form inputs, scroll positions, and component state would reset on every navigation

This foundation enables full route prefetching, instant back/forward navigation, and upcoming features like View Transitions and Gesture APIs.

> **Good to know:** Opt-out strategies are being considered for gradual migration.

If you want to understand how Activity works and the patterns it enables, see the [Activity guide](/docs/app/guides/activity).

## Common issues and fixes

### Dropdown stays open after navigation

Consider a settings page with a dropdown menu. If a user opens the dropdown, navigates to another page, then navigates back, the dropdown is still open. Activity preserved the component state, including `isOpen: true`.

**Fix: Reset state when hidden**

Use Effect cleanup to close the dropdown when Activity hides the component:

```tsx highlight={8-13}
'use client'

import { useState, useLayoutEffect } from 'react'

function SettingsDropdown() {
  const [isOpen, setIsOpen] = useState(false)

  // Close dropdown when this component becomes hidden
  useLayoutEffect(() => {
    return () => {
      setIsOpen(false)
    }
  }, [])

  return (
    <div>
      <button onClick={() => setIsOpen((o) => !o)}>Options</button>
      {isOpen && (
        <ul>
          <li>
            <button>Edit Profile</button>
          </li>
          <li>
            <button>Change Password</button>
          </li>
        </ul>
      )}
    </div>
  )
}
```

When Activity hides this component, the cleanup function runs and resets `isOpen`. When the page becomes visible again, the dropdown is closed. Using `useLayoutEffect` ensures the cleanup runs synchronously before the component is hidden, avoiding any flash of stale state.

You can also use `Link`'s [`onNavigate`](/docs/app/api-reference/components/link#onnavigate) callback to close dropdowns immediately when a navigation link is clicked.

### Dialog doesn't re-run initialization logic

Consider a page with a dialog that focuses its first input when opened:

```tsx
'use client'

import { useState, useRef, useEffect } from 'react'

function ProductTab() {
  const [isDialogOpen, setIsDialogOpen] = useState(false)
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    if (isDialogOpen) {
      inputRef.current?.focus()
    }
  }, [isDialogOpen])

  // ...
}
```

A user opens the dialog (input gets focused), closes it, navigates to another page, navigates back, and opens the dialog again. This time the input doesn't get focused.

The issue is subtle: if the dialog was closed before navigating away, the state is `false`. Returning and opening it sets `isDialogOpen` to `true`, triggering the Effect as expected. However, if the user navigated away _while the dialog was open_, Activity preserved `isDialogOpen: true`. Opening the dialog again sets it to `true` when it's already `true`. No state change means the Effect doesn't re-run.

**Fix: Derive dialog state from something that resets**

Make the open/closed state derive from something outside the preserved component state - like a search param:

```tsx highlight={3,7-9,20,25}
'use client'

import { useSearchParams, useRouter } from 'next/navigation'
import { useEffect, useRef } from 'react'

function ProductTab() {
  const searchParams = useSearchParams()
  const router = useRouter()
  const isDialogOpen = searchParams.get('edit') === 'true'
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    if (isDialogOpen) {
      inputRef.current?.focus()
    }
  }, [isDialogOpen])

  return (
    <div>
      <button onClick={() => router.push('?edit=true')}>Edit Product</button>

      {isDialogOpen && (
        <dialog open>
          <input ref={inputRef} placeholder="Product name" />
          <button onClick={() => router.replace('?', { scroll: false })}>
            Close
          </button>
        </dialog>
      )}
    </div>
  )
}
```

With this approach, `isDialogOpen` derives from the URL rather than component state. When navigating away and returning, the search param is cleared (the URL changed), so `isDialogOpen` becomes `false`. Opening the dialog sets the param, which changes `isDialogOpen` and triggers the Effect.

### Form state persists on "create" routes

A "create new item" page might show stale data when returning to it if form values were preserved by Activity.

**Fix: Use ref cleanup to reset on hide**

Use a callback ref to reset form fields when Activity hides the component:

```tsx
<form
  ref={(form) => {
    // Cleanup function - runs when Activity hides this component
    return () => form?.reset()
  }}
>
  {/* fields */}
</form>
```

This resets the form whenever the user navigates away. See [Resetting state](/docs/app/guides/activity#resetting-state) in the Activity guide for more patterns, including key-based resets for React state.

## State and authentication

Activity preserves local component state (`useState`, DOM input values) across navigations, including authentication changes. This is standard React behavior - props changing (such as receiving a new user) triggers a re-render but does not reset existing state. If your application needs fresh state when users change, you can handle this explicitly.

For logout flows, using `window.location.href` instead of `router.push` triggers a full page reload, clearing all client-side state. For finer control, reset state when the user identity changes:

```tsx
'use client'

import { useState, useEffect, useRef } from 'react'

function UserScopedForm({ userId }: { userId: string | null }) {
  const [draft, setDraft] = useState('')
  const lastUserIdRef = useRef<string | null>(null)

  useEffect(() => {
    if (lastUserIdRef.current !== null && lastUserIdRef.current !== userId) {
      setDraft('') // Reset on user change
    }
    lastUserIdRef.current = userId
  }, [userId])

  return <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />
}
```

Alternatively, key components by user ID to let React handle the reset: `<Form key={userId} />`. See [Resetting state](/docs/app/guides/activity#resetting-state) for more patterns.
