Type something to search...
How to incorporate third-party libraries in Next.js?

How to incorporate third-party libraries in Next.js?

Next.js has become one of the most powerful React frameworks available today. It gives you server-side rendering, static site generation, file-based routing, and an outstanding developer experience right out of the box. However, no framework ships with everything you need. At some point, you will reach for a third-party library to handle animation, form validation, data fetching, UI components, charts, or any number of other requirements.

Knowing how to incorporate those libraries correctly is not merely a convenience — it is a skill that separates developers who build performant, maintainable applications from those who fight mysterious hydration errors and bloated bundles. This guide walks you through every stage of that process, from installation to advanced integration patterns, so you can confidently add any library to your Next.js project.

Why Third-Party Library Integration Deserves Careful Attention

In a plain React project, you install a package, import it, and you are mostly done. Next.js changes the game because it runs code in two environments: the server and the browser. Some libraries assume they are running in a browser and immediately reach for window, document, or localStorage. When Next.js tries to render those libraries on the server during SSR or static generation, it throws an error because those globals simply do not exist in a Node.js environment.

Beyond that, every library you add contributes to your JavaScript bundle. A poorly integrated library can double your client-side bundle size, tank your Core Web Vitals score, and hurt your SEO — the very thing Next.js is designed to protect.

Understanding the integration patterns below gives you the tools to add third-party code without sacrificing the performance benefits that made you choose Next.js in the first place.

Step 1: Install the Library

Every integration starts at the terminal. You use either npm, yarn, or pnpm depending on your project setup.

# npm
npm install library-name

# yarn
yarn add library-name

# pnpm
pnpm add library-name

If the library ships its own TypeScript type definitions, they are included in the package. If not, you can usually install a separate types package:

npm install --save-dev @types/library-name

Always check the library's official documentation for the recommended installation command, because some packages have peer dependencies that you need to install alongside them.

Step 2: Understand the Library's Rendering Environment Requirements

Before you write a single import statement, you need to answer one question: does this library require a browser environment?

You can check this in a few ways:

  • Read the library's documentation for any mention of SSR, server-side rendering, or Next.js compatibility.
  • Search the library's GitHub issues for "Next.js" or "SSR error."
  • Look at the library's source code for direct references to window, document, navigator, or localStorage.

Libraries broadly fall into three categories:

Universal (SSR-safe): These work in both Node.js and the browser. Most utility libraries — lodash, date-fns, zod, axios — fall here. You can import and use them anywhere.

Client-only: These require browser APIs. Examples include animation libraries like GSAP that manipulate the DOM, canvas-based charting libraries, and anything that reads from localStorage on initialisation. You need a special strategy to use these safely.

Server-only: These are designed exclusively for Node.js and should never reach the client bundle. Database clients like Prisma, authentication helpers, and server-side API wrappers belong here.

Step 3: Import Universal Libraries Anywhere

If a library is SSR-safe, you import it exactly as you would in any React project. Place the import at the top of your file and use it directly.

// app/utils/formatDate.ts
import { format } from "date-fns";

export function formatPublishedDate(date: Date): string {
  return format(date, "MMMM dd, yyyy");
}

You can call this function inside Server Components, Client Components, API routes, and middleware without restriction. Universal libraries are the simplest case, and you should not over-engineer their integration.

Step 4: Handle Client-Only Libraries with Dynamic Imports

When a library requires the browser, you use Next.js's built-in dynamic function to defer loading until the component mounts on the client. This prevents the server from attempting to evaluate browser-only code.

Here is the pattern:

// app/components/Chart.tsx
"use client";

import dynamic from "next/dynamic";

const ReactApexChart = dynamic(() => import("react-apexcharts"), {
  ssr: false,
  loading: () => <p>Loading chart...</p>,
});

export default function SalesChart() {
  const series = [{ name: "Revenue", data: [30, 40, 35, 50, 49, 60] }];
  const options = { chart: { type: "line" } };

  return <ReactApexChart type="line" series={series} options={options} />;
}

The ssr: false option tells Next.js to skip server-side rendering entirely for this component. The loading prop gives you a fallback UI while the library loads on the client. This pattern works for any browser-dependent library.

You can also use dynamic imports with named exports:

const { Tooltip } = dynamic(
  () => import("some-ui-library").then((mod) => mod.Tooltip),
  { ssr: false },
);

Step 5: Use the useEffect Hook as an Alternative

Another approach for client-only code is to guard execution inside a useEffect hook. Because useEffect only runs after the component mounts in the browser, you can safely initialise browser-dependent libraries there.

"use client";

import { useEffect } from "react";

export default function MapComponent() {
  useEffect(() => {
    // Leaflet requires a browser — safe to import here
    const L = require("leaflet");
    const map = L.map("map-container").setView([51.505, -0.09], 13);
    L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png").addTo(
      map,
    );
  }, []);

  return <div id="map-container" style={{ height: "400px" }} />;
}

This approach works well for imperative libraries that do not return React components. The require call inside useEffect means the module never executes during SSR.

Step 6: Restrict Server-Only Libraries to the Server

Some libraries should never reach your client bundle. Exposing database credentials, secret keys, or server-side logic to the browser is a serious security risk. Next.js gives you two powerful tools to prevent this.

The server-only package:

npm install server-only

Import it at the top of any module that should remain on the server:

// lib/db.ts
import "server-only";
import { PrismaClient } from "@prisma/client";

export const prisma = new PrismaClient();

If any Client Component or client-side code ever imports from this file, Next.js throws a build-time error. You catch the mistake before it reaches production.

The use server directive:

In the App Router, you can mark entire files or individual functions as Server Actions using the 'use server' directive. Functions marked this way only execute on the server, even when called from a Client Component.

// app/actions/createUser.ts
"use server";

import { prisma } from "@/lib/db";

export async function createUser(data: { name: string; email: string }) {
  return await prisma.user.create({ data });
}

Step 7: Configure External Libraries in next.config.js

Some libraries require you to adjust your Next.js configuration to work correctly. Common scenarios include transpiling packages that ship as ESM, adding webpack aliases, or declaring external packages that should not be bundled.

Transpiling ESM packages (Next.js 13+):

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  transpilePackages: ["some-esm-only-library"],
};

module.exports = nextConfig;

Marking packages as server externals:

If a large package should only run on the server and you want to exclude it from the bundle entirely, you can use serverExternalPackages:

const nextConfig = {
  serverExternalPackages: ["heavy-server-library"],
};

Custom webpack configuration:

For more advanced cases, you can extend the webpack config directly:

const nextConfig = {
  webpack: (config) => {
    config.resolve.alias["some-package"] = require.resolve(
      "./mocks/some-package",
    );
    return config;
  },
};

Always restart your development server after changing next.config.js.

Step 8: Integrate UI Component Libraries

UI libraries like Chakra UI, Ant Design, Material UI, and shadcn/ui each have their own Next.js integration requirements. Most require a Provider component that wraps your application. In the App Router, Providers must be Client Components.

Here is how you integrate a Provider cleanly without marking your root layout as a client component:

// app/providers.tsx
"use client";

import { ChakraProvider } from "@chakra-ui/react";

export function Providers({ children }: { children: React.ReactNode }) {
  return <ChakraProvider>{children}</ChakraProvider>;
}
// app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

This pattern keeps your root layout as a Server Component while enabling the client-side context that UI libraries depend on.

Step 9: Load External Scripts with the next/script Component

When a third-party library ships as an external script — analytics tools, payment widgets, live chat SDKs — you use Next.js's built-in Script component rather than a raw <script> tag. This gives you precise control over when the script loads.

// app/layout.tsx
import Script from "next/script";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://cdn.example.com/analytics.js"
          strategy="lazyOnload"
          onLoad={() => console.log("Analytics loaded")}
        />
      </body>
    </html>
  );
}

The strategy prop accepts four values:

  • beforeInteractive — loads before the page becomes interactive. Use this for critical scripts like bot detection.
  • afterInteractive (default) — loads after the page becomes interactive. Use this for tag managers and analytics.
  • lazyOnload — loads during browser idle time. Use this for chat widgets and non-critical scripts.
  • worker — offloads the script to a web worker using Partytown. Use this for third-party scripts that block the main thread.

Choosing the right strategy directly impacts your Lighthouse score and Core Web Vitals, so treat this decision seriously.

Step 10: Manage Global CSS from Third-Party Libraries

Some libraries ship their own CSS files. You import those stylesheets in your root layout or a top-level _app.tsx file (Pages Router) or layout.tsx (App Router).

// app/layout.tsx
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";

If the library's CSS conflicts with your global styles, scope it with a wrapper class:

/* styles/swiper-overrides.css */
.swiper-wrapper .swiper-slide {
  border-radius: 8px;
}

For libraries that use CSS-in-JS (like styled-components or Emotion), you need to configure the style registry to collect styles during SSR and flush them to the HTML. Both libraries document this process for Next.js specifically — always follow their official App Router guides.

Step 11: Optimise Bundle Size with Tree Shaking and Barrel Imports

Every library you add increases your JavaScript bundle. You minimise this impact by importing only what you need rather than importing the entire library.

Avoid:

import _ from "lodash";
const result = _.chunk([1, 2, 3, 4], 2);

Prefer:

import chunk from "lodash/chunk";
const result = chunk([1, 2, 3, 4], 2);

Modern bundlers support tree shaking, which automatically removes unused exports from ES module packages. However, not all libraries are written as ES modules. For CommonJS packages, explicit per-function imports remain the most reliable approach.

You can also use Next.js's built-in modularizeImports configuration to automatically transform barrel imports:

// next.config.js
const nextConfig = {
  modularizeImports: {
    "@mui/material": {
      transform: "@mui/material/{{member}}",
    },
    lodash: {
      transform: "lodash/{{member}}",
    },
  },
};

This configuration rewrites your imports at build time, giving you the convenience of named imports without the bundle cost.

Step 12: Handle Hydration Mismatches

A hydration mismatch occurs when the HTML rendered on the server does not match what React renders on the client. Many third-party libraries cause this problem by reading browser state during rendering — viewport dimensions, system colour scheme, or random values.

You have several strategies to address this:

Suppress the warning for intentional mismatches:

<div suppressHydrationWarning>
  {typeof window !== "undefined" ? window.innerWidth : null}
</div>

Use this sparingly. It silences the warning but does not fix the underlying cause.

Mount-based rendering:

"use client";

import { useState, useEffect } from "react";
import { ThemeProvider } from "some-theme-library";

export function ClientThemeProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return <>{children}</>;

  return <ThemeProvider>{children}</ThemeProvider>;
}

This pattern renders the children without the Provider on the server and only activates the Provider once the component mounts on the client, eliminating the mismatch.

Step 13: Test Your Integration in Both Development and Production

Development mode in Next.js is more forgiving than production. Always test your third-party library integration with a production build before deploying:

npm run build
npm run start

Check the terminal output for warnings about SSR failures, missing exports, or conflicting packages. Open your browser's developer tools and inspect the Network tab to confirm that large libraries load lazily where expected. Run Lighthouse to verify that your performance scores remain acceptable.

Pay special attention to:

  • Console errors in the browser, particularly hydration warnings
  • Build warnings about missing peer dependencies
  • Bundle analyser output to spot unexpectedly large chunks

You can add the bundle analyser with:

npm install --save-dev @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require("@next/bundle-analyzer")({
  enabled: process.env.ANALYZE === "true",
});

module.exports = withBundleAnalyzer({});

Run ANALYZE=true npm run build to open an interactive visual map of your bundle.

Common Integration Examples

Framer Motion

Framer Motion is SSR-compatible in most cases, but the AnimatePresence component and layout animations rely on client-side state. Mark components that use these features with 'use client'.

"use client";

import { motion } from "framer-motion";

export function FadeInCard({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.4 }}
    >
      {children}
    </motion.div>
  );
}

React Hook Form

React Hook Form is a pure client-side library. Add 'use client' to any component that uses its hooks.

"use client";

import { useForm } from "react-hook-form";

export function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const onSubmit = (data: unknown) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email", { required: true })} />
      {errors.email && <span>Email is required</span>}
      <button type="submit">Submit</button>
    </form>
  );
}

Recharts

Recharts accesses browser APIs internally, so you must disable SSR when importing it dynamically.

"use client";

import dynamic from "next/dynamic";

const LineChart = dynamic(
  () => import("recharts").then((mod) => mod.LineChart),
  { ssr: false },
);

const Line = dynamic(() => import("recharts").then((mod) => mod.Line), {
  ssr: false,
});

Key Takeaways

Incorporating third-party libraries into Next.js is not complicated once you understand the rendering model. You identify where the library runs, choose the correct import strategy, and configure your build accordingly. Here is a quick reference:

ScenarioStrategy
Universal libraryImport directly anywhere
Client-only libraryUse dynamic with ssr: false
Library initialisation in hooksUse useEffect with require
Server-only libraryUse server-only package or 'use server'
External scriptUse next/script with the right strategy
UI library ProviderWrap in a 'use client' Provider component
Bundle size concernUse modular imports or modularizeImports config

Following these patterns consistently will keep your Next.js application fast, secure, and free from SSR-related errors — regardless of which libraries you choose to integrate.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper