5 Common Pitfalls with Server Components in Next13 (with examples)
Tags: nextjs, webdev, react, javascript
Published: 2023-07-06 18:47:15

The [**App Router**](https://nextjs.org/docs/app?ref=propelauth.com) is a new feature in NextJS 13, and it was recently switched to being the recommended option when you create a new NextJS application. This effectively replaces the [**Pages Router**](https://nextjs.org/docs/pages?ref=propelauth.com), and NextJS has documentation on how you can [**incrementally switch over**](https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration?ref=propelauth.com).

The most notable difference in the App Router is that all components, by default, are [**Server Components**](https://nextjs.org/docs/getting-started/react-essentials?ref=propelauth.com#server-components) instead of **Client Components.**

Server Components have a few big differences from their client component equivalent, and with that comes many possible points of confusion. Let’s look at a few common problems:

## **1\. Trying to use React Hooks in Server Components**

Let’s start with a really simple example. I just want to fetch and display a [**cat fact**](https://catfact.ninja/fact?ref=propelauth.com). If you have used React before, this pattern of using `useEffect` to trigger an initial fetch is probably pretty familiar:

```jsx
import {useEffect, useState} from "react";

export default function Demo() {
    const [catFact, setCatFact] = useState<string | null>(null)

    useEffect(() => {
        fetch("https://catfact.ninja/fact")
            .then(response => response.json())
            .then(catFactJson => setCatFact(catFactJson["fact"]))
    }, [])

    if (catFact) {
        return <div>{catFact}</div>
    } else {
        return <div>Loading...</div>
    }
}
```

We’ll place this under `app/demo/page.tsx` and visit [**localhost:3000/demo**](http://localhost:3000/demo) awaiting our cat fact, only to get:

![ReactServerComponentsError](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tvsrcugdtmen7pbyi3qu.png)

a `ReactServerComponentsError`.

The problem is that we are not able to use hooks like **useState** or **useEffect** in server components, as they don’t allow for any interactivity.

Now notably, there are actually two ways to fix the issue:

### **Fix: Add “use client” to the top of the file**

Adding `"use client"` basically enables us to fall back to the behavior you are familiar with, whether you are coming from the Pages Router or just a normal React background.

The component is rendered in the browser, fetches will happen in the browser, and you can see it in your network tab.

![console logs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2dcj552apg27a23642qd.png)

This should all sound exceedingly normal, other than needing to add `"use client"` to the top of our file.

### **Alternative Fix: Convert it to a server component**

In some cases, the hooks we were using weren’t necessary. In this specific case, we were just fetching data once when the component mounted. We could rewrite this more succinctly as a server component:

```tsx
export default async function Demo() {
    const catFactResponse = await fetch("https://catfact.ninja/fact")
    const catFactJson = await catFactResponse.json()
    return <div>{catFactJson["fact"]}</div>
}
```

## **1b - Using Event Handlers (like onClick) in Server Components**

To add to the “server components don’t allow for interactivity” idea, this server component won’t work:

```tsx
export default function Demo() {
    return <button onClick={() => console.log("hi")}>
        A normal button
    </button>
}
```

We go ahead and try to load this component, only to be met with:


![no event handlers allowed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6jrqyrdw3mk7cexo5zg6.png)


“Error: Event handlers cannot be passed to Client Component props”

and

“If you need interactivity, consider converting part of this to a Client Component.”

### **Fix: Add “use client” for interactive components**

Anything interactive (a handler for clicking a button, a hook like `useEffect`) can only be inside of a client component. In this case, our normal button needs “use client” up top. There isn’t a server equivalent.

## **2\. Importing a server component directly into a client component**

Let’s say we want to take our previous example where we fetch cat facts and change it so we get a new Cat Fact every time the user clicks a button. We want something like this:

![cat fact generator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7gvi52p8ucgxcg86ry93.gif)

We can separate out our cat fact server component and pass some value in to trigger a re-render:

```tsx
type Props = {
    heartbeat: number
}

// Note: This is incorrect, don't do this
export default async function CatFact({heartbeat}: Props) {
    const catFactResponse = await fetch("https://catfact.ninja/fact")
    const catFactJson = await catFactResponse.json()
    return <div>{catFactJson["fact"]}</div>
}
```

```tsx
"use client"

import {useState} from "react";
import CatFact from "@/components/CatFact";

export default function Demo() {
    const [heartbeat, setHeartbeat] = useState(0)

    return <div>
        <CatFact heartbeat={heartbeat}/>
        <button onClick={() => setHeartbeat(h => h + 1)}>New Fact Please!</button>
    </div>
}
```

Now if you go to the page, you will see a cat fact, but the button doesn’t appear to work? And the network console tells a very different story:

![rate limited console logs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/58xvo9vz0ikqutpo0sw4.png)

There’s a lot to unpack here, namely:

* Why are requests being made from the client? I’m using server components, no?
    
* Why are there so many requests being made, to the point where we are getting rate limited? (really sorry to the Cat Fact folks for the spam, I thought this was a helpful example)
    

There are a few problems here, but the biggest is that we **cannot import server components into client components**. The specific thing that’s wrong is this line of code:

```tsx
import CatFact from "@/components/CatFact";
```

Our `CatFact` component is now being treated like a client component, which is why we’re seeing the fetches in the browser, and now we’ve effectively just written a really inefficient component that infinitely refetches cat facts.

### **Fix: Convert this to a client component**

It’s natural when a new cool abstraction exists to want to use it as much as possible. That being said, it’s important to note that while server components are useful, you need to know when to use them and when you don’t need them.

In this case, we can just go back to good old regular React components and hooks:

```tsx
"use client"

import {useEffect, useState} from "react";

export default function Demo() {
    const [catFact, setCatFact] = useState<string | null>(null)
    const [heartbeat, setHeartbeat] = useState(0)

    useEffect(() => {
        fetch("https://catfact.ninja/fact")
            .then(response => response.json())
            .then(catFactJson => setCatFact(catFactJson["fact"]))
    }, [heartbeat])

    if (catFact) {
        return <div>
            <div>{catFact}</div>
            <button onClick={() => setHeartbeat(h => h+1)}>New Fact Please!</button>
        </div>
    } else {
        return <div>Loading...</div>
    }
}
```

### **Alternative Fix: Convert it (carefully) to a server component**

It is possible for a server component to be a child of a client component; the only thing we cannot do is **import** the server component into our client code.

What we need to do instead is tell Next.js that our client component will have some child; we just can’t directly reference the `CatFact` component.

Let’s make a new component for this, `CatFactLoader`:

```tsx
"use client"

import React from "react";
import {useRouter} from "next/navigation";

type CatFactLoaderProps = {
    children: React.ReactNode
}

export default function CatFactLoader({children}: CatFactLoaderProps) {
    const router = useRouter()
    return <div>
        {children}
        <button onClick={() => router.refresh()}>New Fact Please!</button>
    </div>
}
```

Update `CatFact`, which will, unfortunately, cache the facts by default:

```tsx
export default async function CatFact() {
    const catFactResponse = await fetch("https://catfact.ninja/fact", {
        // small note - next.js will cache the cat fact, which we can turn off
        cache: 'no-store'
    })
    const catFactJson = await catFactResponse.json()
    return <div>{catFactJson["fact"]}</div>
}
```

and update our `Demo` component with a Server component that composes the two:

```tsx
import CatFactLoader from "@/components/CatFactLoader";
import CatFact from "@/components/CatFact";

export default function Demo() {
    return <CatFactLoader>
        <CatFact />
    </CatFactLoader>
}
```

And this works - however, it’s up to you to decide which pattern you like more.

Speaking of weird patterns, why did we need `router.refresh()` in the client…

## **3\. Assuming Server Components Re-render**

One of the first examples of React code that most people see is the counter example:

```tsx
function MyButton() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Clicked {count} times
    </button>
  );
}
```

And this displays an important concept - when the state is updated, the button re-renders.

Server components obviously don’t have state (see 1 up above), but you can use things like cookies:

```tsx
export default function CookieRenderer() {
    const name = cookies().get("name")?.value
    
    if (name) {
        return <div>Hello, {name}!</div>
    } else {
        return <div>Hello, there!</div>
    }
}
```

You may think that when a new cookie is set, this component would re-render, but it doesn’t. This is maybe more obviously true if your server component makes calls to your database:

```tsx
export default async function Demo() {
    const comments = db.select()
        .from(commentsTable)
        .orderBy(desc(commentsTable.createdAt))
        .limit(10)
        .all()

    return <div>
        {comments.map(comment => <Comment comment={comment} />)}
    </div>
}
```

This is notably something we, at [**PropelAuth**](https://www.propelauth.com/?ref=propelauth.com), dealt with a lot as we provide functions like:

```tsx
export default async function Demo() {
    const user = getUserOrRedirect()
    return <div>Hello {user.firstName}</div>
}
```

but we want to make sure that name changes appropriately trigger re-renders and you don’t have inconsistencies between different tabs within your application.

### **Fix: Rely on routing changes**

While there are [**better options in the works**](https://twitter.com/leeerob/status/1607475053351813120?ref=propelauth.com), `router.refresh()` or `router.replace(router.asPath)` are simple options that will trigger all server components to re-render. You do need to do some work to detect when these changes happen, however.

## **4\. Setting cookies in a server component**

While you can read cookies in server components:

```tsx
export default function CookieDisplayer() {
    const cookieValue = cookies().get("test")?.value
    return <pre>{cookieValue}</pre>
}
```

And you can **call** set:

```tsx
export default async function Header() {
    const abTestingGroup = cookies().get("ab-testing-group")?.value
    if (!abTestingGroup) {
        cookies().set("ab-testing-group", getRandomGroup())
    }
    if (abTestingGroup === "OPTIMISTIC") {
        return <h1>We're the best</h1>
    } else {
        return <h1>Our competitors are the worst</h1>
    }
}
```

This doesn’t actually work:

![no cookies allowed here](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5o754uz81jmphmn8hrkz.png)

The error message here, “Cookies can only be modified in a Server Action or Route Handler” does describe two possible fixes, although we’ll also discuss a third (middleware).

### **Fix: Use Route Handlers**

If you are coming from the pages router, [**route handlers**](https://nextjs.org/docs/app/building-your-application/routing/router-handlers?ref=propelauth.com) are basically just API routes. They are served from a `route.ts` file, and you can set cookies in them:

```tsx
export async function GET() {
    const abTestingGroup = cookies().get("ab-testing-group")?.value
    const defaultGroup = getRandomGroup()
    if (!abTestingGroup) {
        cookies().set("ab-testing-group", defaultGroup, {httpOnly: true, secure: true, sameSite: "strict"})
    }
    return NextResponse.json({"ab-testing-group": abTestingGroup || defaultGroup})
}
```

This obviously is a little less convenient than doing it directly in the component, but you can then read these cookies in your server components.

### **Alternative Fix: Use Middleware**

Another option is to use [**middleware**](https://nextjs.org/docs/app/building-your-application/routing/middleware?ref=propelauth.com) to set the cookie, with a few caveats:

```tsx
export function middleware(request: NextRequest) {
    const abTestingGroup = request.cookies.get("ab-testing-group")?.value

    // TODO: if it's not set... how do I set this group on the request??

    const nextResponse = NextResponse.next()

    // Make sure to set the cookie on the response
    if (!abTestingGroup) {
        nextResponse.cookies.set("ab-testing-group", getRandomGroup(), {httpOnly: true, secure: true, sameSite: "strict"})
    }
    return nextResponse
}
```

This makes it easy to set a cookie on the **response** but not the **request**. While it might seem natural to just do:

```tsx
request.cookies.set(...)
```

This also doesn’t work. There are a few open issues, so it may work someday, but for now, a good workaround is to pass in your own header:

```tsx
export function middleware(request: NextRequest) {
    const abTestingGroup = request.cookies.get("ab-testing-group")?.value
    const defaultGroup = getRandomGroup()

    const headers = new Headers(request.headers)
    headers.append("X-Ab-Testing-Group", abTestingGroup || defaultGroup)

    const nextResponse = NextResponse.next({
        request: {
            headers
        }
    })

    // Make sure to set a on the response
    if (!abTestingGroup) {
        nextResponse.cookies.set("ab-testing-group", defaultGroup, {httpOnly: true, secure: true, sameSite: "strict"})
    }
    return nextResponse
}
```

and then you can use the header instead of the cookie in your server components/routes.

### **Note: Server actions are alpha**

As of writing this, [**server actions**](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions?ref=propelauth.com) are still an alpha feature, so you might want to shy away from this until it’s stable.

## **5\. Trying to pass non-serializable props from server to client**

Let’s say we now wanted our facts to have IDs associated with them. For now, we’re just going to hash the text to get an ID. We decide to write a quick class to encapsulate all the logic:

```tsx
export class CatFactHelper {
    public readonly fact: string
    constructor({ fact }: { fact: string }) {
        this.fact = fact
    }

    public hash(): string {
        return sha256(this.fact)
    }
}
```

We then make a client component to render the fact and its ID:

```tsx
"use client"

import {CatFactHelper} from "@/components/CatFact";

type Props = {
    catFactHelper: CatFactHelper
}

export default function CatFactDisplay({catFactHelper}: Props) {
    return <div>
        ID: {catFactHelper.hash()}<br/>
        Fact: {catFactHelper.fact}
    </div>
}
```

And we update our server component to use this:

```tsx
export default async function CatFact() {
    const catFactResponse = await fetch("https://catfact.ninja/fact", {
        cache: 'no-store'
    })
    const catFactHelper = new CatFactHelper(await catFactResponse.json())
    return <CatFactDisplay catFactHelper={catFactHelper} />
}
```

We are met with this strange error:

![hash not found](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ej5gawn3xivatkmb6ale.png)

What’s also strange is if we log the `catFactHelper` we do see the fact.

![object proto](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iky94qwuew3i8uqk2bba.png)

However, your IDE can give a good hint at what’s going on:

![linting error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r7efbl8hhtc01odqh36e.png)

“Props must be serializable for components in the “use client” entry file”

The problem is our object on the server is serialized to JSON when it’s sent to the client. This object can be serialized to JSON, but the object on the client is equivalent to:

```tsx
{
  "fact": "the cat fact"
}
```

instead of the `CatFactHelper` class with the hash function.

### **Fix: Only send serializable props to client components**

This means no classes or functions. If you really want to use `CatFactHelper`, you should construct it on the client:

```tsx
type Props = {
    fact: string
}

export default function CatFactDisplay({fact}: Props) {
    const catFactHelper = new CatFactHelper({fact})
    return <div>
        ID: {catFactHelper.hash()}<br/>
        Fact: {catFactHelper.fact}
    </div>
}
```

## **Summary**

Server Components are a powerful feature in Next13, but they also have some gotchas to be aware of. In this post, we went over things like re-rendering server components, avoiding importing server components in client components, and when you need to add “use client” to your components.