---
title: "Building an App with Next.js and Electron with Server Components Support"
description: "Learn how to build an app with Next.js and Electron with Server Components support"
date: "2024-08-07"
oldPath: "03-electron-nextjs-ssr"
tags:
  - "electron"
  - "nextjs"
  - "ssr"
  - "rsc"
  - "react-server-components"
  - "server-components"
  - Next.js Electron integration
  - Building apps with Next.js and Electron
  - Next.js Server Components in Electron
  - Electron and Next.js SSR
  - Using React Server Components in Electron
  - Next.js app setup for Electron
  - Optimizing Next.js Electron app
  - React Server Components with Next.js
  - Electron app with Next.js SSR
  - Creating a cross-platform app with Next.js and Electron
  - Next.js and Electron tutorial
  - Server-side rendering in Electron
  - Combining Electron and Next.js
  - Cross-platform development with Next.js
  - Integrating React Server Components in Electron
  - Electron app with Next.js support
  - Building cross-platform apps with Electron
  - Electron with Next.js and React Server Components
  - Efficient SSR with Next.js in Electron
  - Next.js app deployment with Electron
  - Setting up Next.js and Electron project
  - Next.js standalone output for Electron
  - Advanced Electron app with Next.js
  - Using Vite in Electron and Next.js
  - Dynamic content in Electron apps with Next.js
faqs:
  - question: "Can you really use Next.js inside Electron?"
    answer: "Yes. You can bundle an Electron shell and run a Next.js app inside it. The key is deciding whether the Next.js server runs locally (packaged) or the Electron app points to a remote deployment."
  - question: "Do React Server Components work in Electron?"
    answer: "They can, but the constraints are the same as the web: you need a compatible Next.js runtime and you have to be careful about what runs on the server vs. in the renderer process."
  - question: "What’s the biggest gotcha with Electron + Next.js?"
    answer: "Build/runtime boundaries. Electron’s main process, renderer process, and the Next.js server all have different constraints—so keeping APIs, file access, and routing clean matters."
  - question: "Should I use a custom protocol or file:// URLs?"
    answer: "Usually you’ll use a local server URL during dev. In production, many setups prefer a packaged server or a custom protocol to avoid file path quirks and to keep routing predictable."
  - question: "How do I ship updates safely?"
    answer: "Treat the Electron shell and the Next.js app as separate artifacts. Use a proven auto-update mechanism for the shell, and version/deploy the app consistently so you don’t mix incompatible builds."
ogImage: "/images/3-electron-nextjs/nextjs_electron_og.png"
draft: false
---

---

[Image: Building an App with Next.js and Electron with Server Components Support](https://saybackend.com/blog/2024-aug-nextjs-electron-server-components/)

## Introduction

Building an app with Electron and Next.js can be challenging, especially if you want to use React Server Components (RSCs). The typical Static Site Generation (SSG) approach often results in losing a significant amount of functionality and dynamic content.

Moreover, deploying the same codebase to both web and desktop can be complex. Using Vite in Electron and Next.js for SSR is an option, but it becomes cumbersome, especially with the advent of RSCs, using same components for both client and server is not possible without hacks.

In this blog, we'll explore how to build an app with Next.js and Electron with Server Components support.

**Note:**
  If you just want to see the code, you can find it
  [here](https://github.com/spa5k/nextjs_approuter_electron).

## What are Server Components?

Server Components, or RSCs, are a new way to build React apps that offload the rendering of components to the server. This means you can build your app with the same components used for client-side rendering, but the server will render them for you. This can help improve performance and reduce the amount of code you need to write.

RSCs result in sending less JavaScript to the client, allowing the server to handle the heavy lifting of rendering the components. This is particularly useful for building websites that require a lot of dynamic content fetched from the server.

**Note:**
  This is just a quick info, but you can read more about React server components
  [in the official
  docs](https://react.dev/blog/2020/12/21/data-fetching-with-react-server-components).

## How it will look like?

[Image: How it will look like](https://saybackend.com/blog/2024-aug-nextjs-electron-server-components/)

## Setting Up the Project

1. **Create a New Next.js Project:**

```bash
npx create-next-app@latest nextjs-electron
```

2. **Install Dependencies:**

```bash
npm install electron electron-builder tsup nodemon npm-run-all cross-env -D
npm install get-port-please @electron-toolkit/utils
```

3. **Update Next.js Config:**

Update `next.config.js` to output as 'standalone':

```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "standalone", // This line
  reactStrictMode: true,
};

export default nextConfig;
```

> Outputting as 'standalone' results in a smaller package that we can use in Electron, with trimmed `node_modules`. This is also the best way when self-hosting the Next.js app.

4. **Update `package.json`:**

Point to the transpiled file in the `main` key:

```json
{
  "main": "build/main.js"
}
```

Add the following to ensure Electron knows what files to bundle:

```json
{
  "build": {
    "asar": true,
    "executableName": "NextJSElectron",
    "appId": "com.saybackend.nextjs-electron",
    "asarUnpack": [
      "node_modules/next",
      "node_modules/@img",
      "node_modules/sharp",
      "**\\*.{node,dll}"
    ],
    "files": [
      "build",
      {
        "from": ".next/standalone",
        "to": "app",
        "filter": [
          "!**/.env",
          "!**/package.json"
        ]
      },
      {
        "from": ".next/static",
        "to": "app/.next/static"
      },
      {
        "from": "public",
        "to": "app/public"
      }
    ],
    "win": {
      "target": [
        "nsis"
      ]
    },
    "linux": {
      "target": [
        "deb"
      ],
      "category": "Development"
    }
  }
```

5. **Add Scripts to `package.json`:**

```json
"scripts": {
  "next:dev": "next dev",
  "next:build": "next build",
  "next:start": "next start",
  "next:lint": "next lint",
  "format": "dprint fmt",
  "postinstall": "electron-builder install-app-deps",
  "electron:dist": "electron-builder --dir",
  "electron:dist:deb": "electron-builder --linux deb",
  "electron:build": "tsup",
  "build": "run-s next:build electron:build",
  "dist": "run-s next:build electron:dist",
  "dev": "npm-run-all --parallel electron:dev next:dev",
  "electron:build_watch": "tsup --watch",
  "electron:dev": "npm-run-all --parallel electron:build_watch electron:watch",
  "electron:watch": "cross-env NODE_ENV='development' nodemon"
}
```

> Preferably, create a Makefile to run the commands, as it gets too complex to run them individually. You can find one Makefile [here](https://github.com/spa5k/nextjs_approuter_electron/blob/main/Makefile).

6. **Add `tsup.config.ts`:**

```ts
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["./electron/main.ts", "./electron/preload.ts"],
  splitting: false,
  sourcemap: false,
  clean: true,
  cjsInterop: true,
  skipNodeModulesBundle: true,
  treeshake: true,
  outDir: "build",
  external: ["electron"],
  format: ["cjs"],
  bundle: true,
});
```

7. **Add `nodemon.json`:**

```json
{
  "$schema": "https://json.schemastore.org/nodemon.json",
  "exec": "electron .",
  "watch": ["main"],
  "ignore": ["build", "public/build"]
}
```

## Electron Setup

Create a new folder called `electron`, and add two files: `main.ts` and `preload.ts`.

### `main.ts`

```ts
import { is } from "@electron-toolkit/utils";
import { app, BrowserWindow, ipcMain } from "electron";
import { getPort } from "get-port-please";
import { startServer } from "next/dist/server/lib/start-server";
import { join } from "path";

const createWindow = () => {
  const mainWindow = new BrowserWindow({
    width: 900,
    height: 670,
    webPreferences: {
      preload: join(__dirname, "preload.js"),
      nodeIntegration: true,
    },
  });

  mainWindow.on("ready-to-show", () => mainWindow.show());

  const loadURL = async () => {
    if (is.dev) {
      mainWindow.loadURL("http://localhost:3000");
    } else {
      try {
        const port = await startNextJSServer();
        console.log("Next.js server started on port:", port);
        mainWindow.loadURL(`http://localhost:${port}`);
      } catch (error) {
        console.error("Error starting Next.js server:", error);
      }
    }
  };

  loadURL();
  return mainWindow;
};

const startNextJSServer = async () => {
  try {
    const nextJSPort = await getPort({ portRange: [30_011, 50_000] });
    const webDir = join(app.getAppPath(), "app");

    await startServer({
      dir: webDir,
      isDev: false,
      hostname: "localhost",
      port: nextJSPort,
      customServer: true,
      allowRetry: false,
      keepAliveTimeout: 5000,
      minimalMode: true,
    });

    return nextJSPort;
  } catch (error) {
    console.error("Error starting Next.js server:", error);
    throw error;
  }
};

app.whenReady().then(() => {
  createWindow();

  ipcMain.on("ping", () => console.log("pong"));
  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") app.quit();
});
```

### `preload.ts`

```ts
import { contextBridge, ipcRenderer } from "electron";

contextBridge.exposeInMainWorld("electron", {
  ipcRenderer: {
    send: (channel: string, data: any) => ipcRenderer.send(channel, data),
    on: (channel: string, listener: (event: any, ...args: any[]) => void) =>
      ipcRenderer.on(channel, listener),
  },
});
```

### Explanation

Since Next.js 13, they do not provide a way to use a custom server like through Express.js to run the server for Next.js, as it requires RSC support. We can use the server already built by the Next.js team and put it to use here.

The port situation is different from other ways to do an Electron app, as they either use JS files or `index.html`. Here, we need to run the server for SSR and RSC experience. We are using `get-port-please` to get a port for the Next.js server to run on.

```ts
const nextJSPort = await getPort({ portRange: [30011, 50000] }); // It's better to use a range, as it's less likely to be used by other apps.
const webDir = join(app.getAppPath(), "app"); // This is where the Next.js build is stored.

await startServer({
  dir: webDir,
  isDev: false,
  hostname: "localhost",
  port: nextJSPort,
  customServer: true,
  allowRetry: false,
  keepAliveTimeout: 5000,
  minimalMode: true,
});

// These are just default values, you can change them as per your needs.
```

```ts
const loadURL = async () => {
  if (is.dev) {
    mainWindow.loadURL("http://localhost:3000");
  } else {
    try {
      const port = await startNextJSServer();
      mainWindow.loadURL(`http://localhost:${port}`);
    } catch (error) {
      console.error("Error starting Next.js server:", error);
    }
  }
};
```

Here we are checking if the app is in development, or production mode, and load the URL accordingly. No need to run everything in development, since even when we are running the Next.js app on separate process, it can still do everything it needs during dev.

## Running the app

Now, you can run the app by running the following command:

```bash
npm run dev
```

or

```bash
make dev
```

This will start the Next.js, Tsup in watch mode for the electron, and nodemon to watch the transpiled files.

Now you will see the window pop up with the Next.js app running in it. You can now try editing any Next.js file and it will automatically reload the app through HMR. Pretty easy right?

Similarly you can also edit the Electron files and it will reload the app, the window will close and open again with the changes.

[Image: How it will look like](https://saybackend.com/blog/2024-aug-nextjs-electron-server-components/)

As you can see, we are getting a perfectly running server component, a client component, and a way to call electron for native functionalities.

## Sending commands to Electron from Next.js

You can send commands to the electron from the Next.js app by using the `electron` object that we exposed in the preload.ts file.

```ts
import { useEffect } from "react";

export default function Home() {
  useEffect(() => {
    electron.ipcRenderer.send("ping");
  }, []);

  return (
    <div>
      <h1>Hello, World!</h1>
    </div>
  );
}
```

And in the main.ts file, you can listen to the ping command like this:

```ts
ipcMain.on("ping", () => console.log("pong"));
```

And that's it! You have successfully built an app with Next.js and Electron with Server Components support.

Click on the Ping Electron button, and you will see the pong message in the console.

[Image: pong as output in console](https://saybackend.com/blog/2024-aug-nextjs-electron-server-components/)

Try creating a server component, you will see it's working as expected, and the server is doing the heavy lifting of pre rendering the components.

## Pakaging the app

To package the app, you can run the following command:

```bash
npm run electron:dist
```

or

```bash
make electron_dist
```

Current it's only packaging it as the dir, you can change it to deb, or dmg, or any other format you want.

## GitHub Repository

[spa5k/nextjs_approuter_electron](spa5k/nextjs_approuter_electron)
