
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, orlocalStorage.
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:
| Scenario | Strategy |
|---|---|
| Universal library | Import directly anywhere |
| Client-only library | Use dynamic with ssr: false |
| Library initialisation in hooks | Use useEffect with require |
| Server-only library | Use server-only package or 'use server' |
| External script | Use next/script with the right strategy |
| UI library Provider | Wrap in a 'use client' Provider component |
| Bundle size concern | Use 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.


