DEV Community

pythonassignmenthelp.com
pythonassignmenthelp.com

Posted on

Why Our React Server Components Broke During a Next.js 15 Upgrade

You know the feeling: you finally get sign-off to upgrade your Next.js app, thinking it’ll be a weekend project. Maybe you’re excited about faster builds, or you just want to keep up with the latest. But then, out of nowhere, half your Server Components explode with cryptic errors. That’s exactly what happened to us during our jump to Next.js 15. If you’re running Server Components, or thinking about upgrading, I’m here to walk you through what actually broke, why, and how we got things running smoothly again.

The Upgrade That Broke Our Build

Our app isn’t huge, but we rely heavily on React Server Components (RSC). We love how RSC lets us fetch data on the server, cut down bundle sizes, and generally feel like we’re living in the future. Next.js 15 promised better stability and some under-the-hood improvements, so we thought, “What could go wrong?”

Turns out, a lot. The thing is, Server Components are still evolving fast, and some patterns that "just worked" in Next.js 14 suddenly started throwing errors or behaving weirdly after the upgrade.

The First Error: “You cannot use hooks inside a Server Component”

After the upgrade, a bunch of our pages blew up with errors like:

Error: Hooks can only be used inside Client Components.
Enter fullscreen mode Exit fullscreen mode

We’d seen this before, but we were careful: all our useState, useEffect, and useContext calls were inside use client components (or so we thought). What changed?

Here’s the catch: Next.js 15 tightened how it detects Server vs Client Components. In previous versions, if you accidentally imported a Client Component into a Server Component, it might have silently worked (or failed later). Now, it fails fast.

Example: Importing a Client Component Inside a Server Component

Suppose you have a file structure like this:

/components
  - UserProfile.server.jsx
  - UserSettings.client.jsx
Enter fullscreen mode Exit fullscreen mode

Previously, you might have written:

// UserProfile.server.jsx

import UserSettings from './UserSettings.client';

export default function UserProfile({ userId }) {
  // ...fetch user data from DB
  return (
    <div>
      {/* Some server-rendered stuff */}
      <UserSettings userId={userId} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This should work, right? You’re importing a Client Component into a Server Component, which is allowed.

But here’s where it went wrong for us: if UserSettings.client.jsx (or anything it imports) tries to use another Server Component, or imports server-only code, you’ll get runtime errors. The error messages in Next.js 15 are much stricter and earlier.

What we learned: Be absolutely sure your Client Components and their imports are “clean” — i.e., they don’t pull in any server-only code.

Fix: Double-Check Component Boundaries

We ended up splitting some utility functions into /lib/server and /lib/client folders just to be clear about what could be imported where.

// /lib/server/getUserData.js
export async function getUserData(userId) {
  // Only server-side DB fetches here
}

// /lib/client/formatDisplayName.js
export function formatDisplayName(user) {
  // Only pure JS, no Node APIs
}
Enter fullscreen mode Exit fullscreen mode

Then, in our components, we only import from the right side:

// UserProfile.server.jsx
import { getUserData } from '../lib/server/getUserData';

// UserSettings.client.jsx
import { formatDisplayName } from '../lib/client/formatDisplayName';
Enter fullscreen mode Exit fullscreen mode

This sounds obvious, but with a big codebase, it’s easy to let these boundaries blur.

The Second Error: “Dynamic server usage: fetch() is not allowed in Client Components”

Next.js 15 got more aggressive about enforcing where you can call fetch(). We’d gotten used to calling fetch() in Server Components, but somehow, after the upgrade, we started getting errors like:

Error: fetch() is not allowed in Client Components. Move your data fetching to a Server Component.
Enter fullscreen mode Exit fullscreen mode

How did this happen? We traced it back to a Client Component that imported a helper function from a shared util file. That helper happened to call fetch(). In Next.js 14, it worked — but it was never really safe.

Example: Shared Helper with fetch

// /lib/apiHelpers.js

export async function getWeather(city) {
  // This is a server-side API call
  const res = await fetch(`https://api.weather.com/v1/${city}`);
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode
// WeatherWidget.client.jsx

import { getWeather } from '../lib/apiHelpers';

export default function WeatherWidget({ city }) {
  // ❌ This will error in Next.js 15
  const [weather, setWeather] = useState(null);

  useEffect(() => {
    getWeather(city).then(setWeather);
  }, [city]);

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Why the error? Because getWeather is calling fetch(), which Next.js 15 now blocks in Client Components. In Next.js 14, this might have slipped through, but it was always a footgun.

Fix: Move Data Fetching to Server Components

We refactored like this:

// WeatherWidgetWrapper.server.jsx

import WeatherWidget from './WeatherWidget.client';
import { getWeather } from '../lib/apiHelpers';

export default async function WeatherWidgetWrapper({ city }) {
  // ✅ Fetch on server
  const weather = await getWeather(city);
  return <WeatherWidget weather={weather} />;
}
Enter fullscreen mode Exit fullscreen mode
// WeatherWidget.client.jsx

export default function WeatherWidget({ weather }) {
  // Now just display, no fetching
  return <div>{weather.temp}°C</div>;
}
Enter fullscreen mode Exit fullscreen mode

This pattern — “fetch on the server, hand off to the client” — became our new default. It’s a bit more verbose, but way safer.

The Surprise: Breaking Changes in Route Handlers

One thing we didn’t expect: Next.js 15 changed how Route Handlers (the new /app/api/route.js stuff) interact with Server Components. We had a couple of Server Components that directly imported handler utilities from our API routes. This just flat-out stopped working.

Example: Importing from Route Handlers (What Not to Do)

// /app/api/users/route.js

export async function getUsers() {
  // DB fetch logic
}
Enter fullscreen mode Exit fullscreen mode
// /components/UserList.server.jsx

import { getUsers } from '../app/api/users/route';

export default async function UserList() {
  const users = await getUsers();
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Why is this bad? In Next.js 15, Route Handlers are built as isolated modules with their own context. Importing server logic from a route file can break assumptions, and can even cause circular dependency issues or deployment failures.

Fix: Share Logic, Not Route Files

We moved shared logic into a true shared module:

// /lib/server/userQueries.js

export async function getUsers() {
  // DB logic here
}
Enter fullscreen mode Exit fullscreen mode

Then, in both the route handler and the Server Component:

// /app/api/users/route.js
import { getUsers } from '../../../lib/server/userQueries';

export async function GET(request) {
  const users = await getUsers();
  // ...
}

// /components/UserList.server.jsx
import { getUsers } from '../lib/server/userQueries';

export default async function UserList() {
  const users = await getUsers();
  // ...
}
Enter fullscreen mode Exit fullscreen mode

This keeps boundaries clear and avoids subtle bugs.

Common Mistakes (We Made Them Too)

  1. Assuming shared files will “just work” everywhere.

    If you call fetch() or use Node APIs in a file, importing it into a Client Component will break. It’s tempting to keep all your helpers in one place, but split them by environment as your app grows.

  2. Using dynamic imports to “cheat” boundaries.

    We tried to lazily import some server-only code in Client Components, thinking it would avoid errors. Didn’t work — you’ll still get runtime errors or broken builds.

  3. Not updating dependencies.

    Next.js 15 depends on newer versions of React and some other ecosystem libraries. We had a stale react version and got mysterious errors until we aligned all versions. Always check the Next.js upgrade guide for peer dependency requirements.

Key Takeaways

  • Next.js 15 is much stricter about Client vs Server boundaries. Don’t assume patterns from previous versions will work.
  • Never call fetch() or use Node APIs in Client Components, even via helpers.
  • Route Handlers should not be used as shared logic modules. Move business logic into /lib/server and import where needed.
  • Organize your codebase clearly: /lib/server for server-only logic, /lib/client for client-safe helpers.
  • Always double-check your dependencies when upgrading. Version mismatches cause subtle, hard-to-debug issues.

Closing Thoughts

Upgrading isn’t always glamorous, but it’s how we keep our projects healthy. If you’re using React Server Components in Next.js, treat boundaries as sacred — your future self (and your team) will thank you. If you hit your own weird errors upgrading, you’re not alone. Keep going, and happy coding.


If you found this helpful, check out more programming tutorials on our blog. We cover Python, JavaScript, Java, Data Science, and more.

Top comments (0)