Skip to main content
RapidDev - Software Development Agency
v0-issues

Preventing useEffect from causing side effects in V0

V0 generates useEffect hooks that often lack cleanup functions, access browser-only APIs during server-side rendering, and have incorrect or missing dependency arrays. In Next.js App Router, Client Components are server-rendered before hydration, which means useEffect code that accesses localStorage, window, or document causes errors during SSR. Fix these by adding cleanup functions to every effect that creates subscriptions or timers, guarding browser APIs with typeof window checks, and specifying exact dependency arrays to prevent unintended re-runs.

Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read10-20 minutesV0 with Next.js App Router, React 18+March 2026RapidDev Engineering Team
TL;DR

V0 generates useEffect hooks that often lack cleanup functions, access browser-only APIs during server-side rendering, and have incorrect or missing dependency arrays. In Next.js App Router, Client Components are server-rendered before hydration, which means useEffect code that accesses localStorage, window, or document causes errors during SSR. Fix these by adding cleanup functions to every effect that creates subscriptions or timers, guarding browser APIs with typeof window checks, and specifying exact dependency arrays to prevent unintended re-runs.

Why useEffect causes side effects in V0-generated code

V0's AI generates useEffect hooks for data fetching, event listeners, timers, and localStorage access. However, the generated code frequently has three problems: missing cleanup functions that cause memory leaks when components unmount, direct access to browser APIs like localStorage and window that fail during Next.js server-side rendering, and incorrect dependency arrays that cause effects to run too often or not often enough. In Next.js App Router, even Client Components with 'use client' are server-rendered first, which means any useEffect body that references browser globals will not fail during the effect itself (useEffect only runs on the client), but code outside useEffect — like state initialization — will fail during SSR. The most dangerous pattern is effects that create subscriptions, intervals, or event listeners without returning a cleanup function.

  • Missing cleanup function in useEffect — event listeners, intervals, and subscriptions accumulate on re-renders and navigation
  • State initialized with localStorage.getItem() outside useEffect crashes during SSR
  • Empty dependency array on effects that should re-run when props or state change
  • Multiple useEffects that depend on each other create cascading update chains
  • V0 generates window.addEventListener without a corresponding removeEventListener cleanup

Error messages you might see

ReferenceError: localStorage is not defined

State initialization uses localStorage.getItem() directly, which runs during server-side rendering where localStorage does not exist. Move localStorage access into a useEffect.

Warning: Can't perform a React state update on an unmounted component.

An async operation (fetch, setTimeout) inside useEffect resolves after the component has unmounted, attempting to call setState on a component that no longer exists. Add cleanup that cancels or ignores the async result.

do not use 'new' for side effects — constructor call not assigned to a variable

An ESLint warning that appears when useEffect creates objects with 'new' (like new EventSource or new WebSocket) without assigning them to a variable for cleanup. Assign the instance to a variable and close it in the cleanup function.

Before you start

  • A V0 project with components that use useEffect hooks
  • Access to the V0 code editor to review and modify effect hooks
  • Basic understanding of React component lifecycle and cleanup

How to fix it

1

Add cleanup functions to effects that create subscriptions or timers

Without cleanup, event listeners, intervals, and subscriptions persist after the component unmounts or re-renders. This causes memory leaks, duplicate event handlers, and state updates on unmounted components.

Return a cleanup function from every useEffect that creates a side effect. The cleanup function should remove event listeners, clear intervals, close connections, and cancel ongoing operations.

Before
typescript
"use client";
import { useEffect, useState } from "react";
export function WindowSize() {
const [width, setWidth] = useState(0);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
handleResize();
// No cleanup — listener accumulates on every re-render
}, []);
return <p>Width: {width}px</p>;
}
After
typescript
"use client";
import { useEffect, useState } from "react";
export function WindowSize() {
const [width, setWidth] = useState(0);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
handleResize();
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return <p>Width: {width}px</p>;
}

Expected result: The resize listener is removed when the component unmounts, preventing memory leaks.

2

Guard browser API access for SSR compatibility

In Next.js App Router, Client Components are server-rendered before hydrating on the client. Code that runs during module initialization or state initialization (outside useEffect) must not access browser APIs. useEffect itself only runs on the client, but state initializers run on both server and client.

Move all localStorage, window, and document access into useEffect hooks. Initialize state with default values that work on the server, then update with browser-specific values in useEffect.

Before
typescript
"use client";
import { useState } from "react";
export function ThemeToggle() {
// Crashes during SSR — localStorage not available
const [theme, setTheme] = useState(
localStorage.getItem("theme") || "light"
);
return <button onClick={() => setTheme("dark")}>{theme}</button>;
}
After
typescript
"use client";
import { useState, useEffect } from "react";
export function ThemeToggle() {
const [theme, setTheme] = useState("light");
useEffect(() => {
const saved = localStorage.getItem("theme");
if (saved) setTheme(saved);
}, []);
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);
return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>{theme}</button>;
}

Expected result: The component renders with the default 'light' theme during SSR and updates to the saved theme after hydration.

3

Cancel async operations in the cleanup function

If a fetch request or timeout resolves after the component unmounts, the setState call throws a warning and can cause bugs. Use AbortController for fetch requests and clearTimeout for timers.

Create an AbortController before the fetch call and pass its signal to the fetch options. In the cleanup function, call controller.abort() to cancel pending requests.

Before
typescript
"use client";
import { useEffect, useState } from "react";
export function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
// If userId changes quickly, old fetch results overwrite new ones
}, [userId]);
return <div>{user?.name}</div>;
}
After
typescript
"use client";
import { useEffect, useState } from "react";
export function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(data => setUser(data))
.catch(err => {
if (err.name !== "AbortError") throw err;
});
return () => controller.abort();
}, [userId]);
return <div>{user?.name}</div>;
}

Expected result: When userId changes, the previous fetch is cancelled before the new one starts, preventing stale data.

4

Specify exact dependency arrays for controlled effect execution

An empty dependency array runs the effect once on mount. A missing dependency array runs the effect after every render. Incorrect dependencies cause effects to either run too often (performance waste, potential loops) or not often enough (stale data).

List every variable from the component scope that the effect reads. If the effect fetches data based on a prop, that prop must be in the dependency array. Use the ESLint exhaustive-deps rule to catch missing dependencies.

Before
typescript
// Effect depends on searchQuery but it's not in the array
useEffect(() => {
fetch(`/api/search?q=${searchQuery}`)
.then(res => res.json())
.then(setResults);
}, []); // Never re-runs when searchQuery changes
After
typescript
// searchQuery in dependency array — effect re-runs when it changes
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${searchQuery}`, { signal: controller.signal })
.then(res => res.json())
.then(setResults)
.catch(err => {
if (err.name !== "AbortError") throw err;
});
return () => controller.abort();
}, [searchQuery]); // Re-runs when searchQuery changes

Expected result: Search results update whenever the search query changes, and previous requests are properly cancelled.

Complete code example

hooks/use-local-storage.ts
1"use client";
2
3import { useState, useEffect, useCallback } from "react";
4
5/**
6 * SSR-safe localStorage hook with proper cleanup.
7 * Returns the stored value and a setter function.
8 * Initializes with defaultValue during SSR and
9 * reads from localStorage after hydration.
10 */
11export function useLocalStorage<T>(
12 key: string,
13 defaultValue: T
14): [T, (value: T | ((prev: T) => T)) => void] {
15 const [storedValue, setStoredValue] = useState<T>(defaultValue);
16
17 // Read from localStorage after mount
18 useEffect(() => {
19 try {
20 const item = localStorage.getItem(key);
21 if (item !== null) {
22 setStoredValue(JSON.parse(item));
23 }
24 } catch (error) {
25 console.warn(`Error reading localStorage key "${key}":`, error);
26 }
27 }, [key]);
28
29 // Write to localStorage when value changes
30 const setValue = useCallback(
31 (value: T | ((prev: T) => T)) => {
32 setStoredValue((prev) => {
33 const nextValue =
34 value instanceof Function ? value(prev) : value;
35 try {
36 localStorage.setItem(key, JSON.stringify(nextValue));
37 } catch (error) {
38 console.warn(
39 `Error writing localStorage key "${key}":`,
40 error
41 );
42 }
43 return nextValue;
44 });
45 },
46 [key]
47 );
48
49 return [storedValue, setValue];
50}

Best practices to prevent this

  • Return a cleanup function from every useEffect that creates event listeners, intervals, subscriptions, or timers
  • Initialize state with SSR-safe defaults and sync with browser APIs (localStorage, window) inside useEffect
  • Use AbortController to cancel fetch requests when the component unmounts or dependencies change
  • Specify exact dependency arrays — never omit the array (runs every render) unless intentional
  • Create custom hooks like useLocalStorage to encapsulate SSR-safe browser API access patterns
  • When V0 generates a useEffect, always verify the cleanup function and dependency array before accepting the code
  • For complex effect management across many components, RapidDev can audit and optimize the entire effect architecture

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

My V0 Next.js app has useEffect hooks that cause 'localStorage is not defined' during SSR and 'state update on unmounted component' warnings. How do I make all useEffect hooks SSR-safe with proper cleanup functions?

Frequently asked questions

Why does useEffect cause 'localStorage is not defined' in my V0 app?

The error is not from useEffect itself — useEffect only runs on the client. The error comes from state initialization (useState(localStorage.getItem(...))) which runs during server-side rendering. Move localStorage reads into a useEffect and initialize state with a default value.

Do I need cleanup functions for every useEffect?

Not every effect needs cleanup, but any effect that creates a subscription, event listener, interval, timer, or WebSocket connection must return a cleanup function. Effects that only read data without ongoing subscriptions do not need cleanup.

How do I clean up side effects in useEffect?

Return a function from useEffect that reverses the side effect: removeEventListener for listeners, clearInterval for intervals, abort() for fetch requests, close() for WebSocket connections, and unsubscribe() for subscriptions.

Why does React Strict Mode run my useEffect twice?

In development, React Strict Mode double-invokes useEffect to help you find missing cleanup functions. The first invocation's cleanup runs before the second invocation. In production, useEffect runs once as expected. If double-invocation causes bugs, your cleanup function is incomplete.

Can RapidDev fix useEffect issues across my V0 project?

Yes. RapidDev can audit all useEffect hooks in your V0 project, add proper cleanup functions, fix SSR compatibility issues, optimize dependency arrays, and create reusable custom hooks for common patterns like localStorage, window events, and data fetching.

Should I use useEffect for data fetching in Next.js App Router?

For initial page data, use Server Components with async/await instead of client-side useEffect. Use useEffect for data that needs to be fetched on the client after user interaction (search, pagination, filtering) or for real-time subscriptions.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your issue.

Book a free consultation

Need help with your Lovable project?

Our experts have built 600+ apps and can solve your issue fast. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.