✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

Ecosystem14 min read

12 Next.js Libraries Worth Adding to Your Stack

April 4, 2025Updated on September 14, 2026
Awesome Next.js Libraries

12 Next.js Libraries Worth Adding to Your Stack

Next.js 16 ships Turbopack as the default bundler, requires React 19, and treats Server Components as the default in the App Router (Next.js 16 release notes). That makes some library advice written for Next.js 13 and 14 outdated. Module-level Zustand stores can share state between requests, ssr: false no longer works in Server Components, React Query was renamed, and Framer Motion became Motion.

This guide covers 12 Next.js libraries for data fetching, client state, UI components, forms, animation, auth, database access, and SEO, with the setup that works on the App Router today. It is written for full-stack developers building content-driven apps, often with Strapi as the headless CMS behind them. For each library, you get the current install command, the App Router gotcha that trips people up, and where it sits next to Strapi's API.

In brief:

  • TanStack Query and SWR still earn a place on the App Router, but only for client-side jobs like polling, infinite scroll, and optimistic updates; Server Components handle public content.
  • Chakra UI v3, Tailwind CSS v4, and Radix UI all changed their install steps; use the current snippets below.
  • Zod 4 cut its core bundle to 5.36 kB gzipped and pairs with React Hook Form through @hookform/resolvers 5.1.0 or later.
  • Auth.js now belongs to Better Auth, Prisma 7 requires a driver adapter, and next-seo's README points App Router users to the native Metadata API.

How to Evaluate Next.js Libraries for the App Router

Measure bundle impact before anything else. Install @next/bundle-analyzer, wrap your config, and run:

ANALYZE=true npm run build

The Package Bundling guide covers the setup:

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});

The report shows which dependencies a small-looking package drags along. For libraries with large barrel files, experimental.optimizePackageImports rewrites imports so only used modules load; @headlessui/react, lucide-react, and date-fns are optimized by default, while @chakra-ui/react has to be added manually (optimizePackageImports reference).

Next, work out which side of the 'use client' boundary the library lives on. Anything that uses hooks, React Context, event handlers, or browser APIs must render inside a Client Component (Server and Client Components). Libraries that have not added the directive themselves get a one-line wrapper:

// components/Carousel.tsx
'use client';
import { Carousel } from 'acme-carousel';
export default Carousel;

The same rule governs lazy loading: next/dynamic with ssr: false is not allowed in Server Components, so move that call into a Client Component (Lazy Loading guide).

Finally, check maintenance and ownership rather than star counts. Several libraries below changed hands or names recently, and star counts alone do not show whether a repository is in active development or security-only maintenance. The performance mistakes guide shows what happens when heavy client libraries land where a Server Component would do.

Data Fetching and State Management Libraries

TanStack's own SSR guide recommends that new Server Components applications "start with framework-provided data-fetching tools and avoid bringing in React Query until actually needed" (Advanced SSR guide). For Strapi content, a Server Component calling fetch against the REST API with populate and fields parameters covers most pages (populate and select docs). Since Next.js 15, fetch defaults to no-store, so tag your requests and revalidate on publish (Next.js 15 upgrade guide). The SSR vs. SSG comparison helps match rendering mode to content type.

Client libraries still make sense for polling, infinite scroll, optimistic updates, and private user pages.

1. TanStack Query

@tanstack/react-query v5 pulls around 46 million weekly npm downloads (npm registry). Two things from older tutorials no longer work: the package was renamed from react-query in v4, and v5 removed the positional useQuery(key, fn) signature in favor of one options object (v5 migration guide).

// components/UserProfile.tsx
'use client';
import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }: { userId: string }) {
  const { data, error, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{data.name}</div>;
}

On the App Router, prefetch in a Server Component and pass the dehydrated cache through HydrationBoundary (the older Hydrate component is gone). Since v5.40.0, the prefetch no longer needs an await, and useSuspenseQuery in the Client Component streams the result. The same guide advises against fetching inside a queryFn through Server Actions, and warns that a client refetch cannot revalidate Server Component output. Keep CMS content on the server; use TanStack Query for data that changes while the user is on the page.

2. SWR

SWR, maintained by Vercel, returns cached data first, revalidates in the background, then updates (getting started docs). Its hooks (useSWR, useSWRInfinite, useSWRMutation) only run in Client Components; only SWRConfig, preload, and unstable_serialize can be imported in Server Components (SWR with Next.js).

// components/Profile.tsx
'use client';
import useSWR from 'swr';

function Profile({ userId }) {
  const { data, error, isLoading } = useSWR(`/api/user/${userId}`, fetcher);
  if (error) return <div>failed to load</div>;
  if (isLoading) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

SWR 2.5 added an experimental App Router pattern: call preload() in a Server Component layout and pass the result as cacheData on SWRConfig so client hooks start warm. The fallback prop from older tutorials is now cacheData. SWR's docs position it for private, user-specific pages where SEO does not apply, which is a sensible dividing line: SWR for dashboards, Server Components for public Strapi content.

3. Zustand

Zustand handles client state with a store hook. v5 removed the default export, so import { create } (v5 migration docs). Immer support is an optional middleware that needs npm install immer, not a built-in (Immer middleware reference).

The older claim that Zustand needs no provider is wrong for the App Router. Zustand's Next.js guide states the store "should not be defined as a global variable. Instead, the store should be created per request," and that Server Components must not read from or write to it. The documented pattern is a store factory plus a Client Component provider:

// providers/CounterStoreProvider.tsx
'use client';
import { createContext, useState } from 'react';
import { createStore } from 'zustand/vanilla';

const createCounterStore = () =>
  createStore<CounterStore>()((set) => ({
    count: 0,
    incrementCount: () => set((state) => ({ count: state.count + 1 })),
  }));

export const CounterStoreContext = createContext<CounterStoreApi | null>(null);

export function CounterStoreProvider({ children }: { children: React.ReactNode }) {
  const [store] = useState(() => createCounterStore());
  return <CounterStoreContext.Provider value={store}>{children}</CounterStoreContext.Provider>;
}

Use it for UI state that survives client navigations: cart drawers, filters, and open panels. CMS content stays in Server Components.

Next.js Component Libraries and Styling Options

Next.js component libraries fall into three camps: styled systems (Chakra UI), utility CSS plus unstyled behavior (Tailwind with Headless UI), and unstyled primitives you skin yourself (Radix UI, with shadcn/ui built on top). All three shipped breaking setup changes.

4. Chakra UI

Chakra UI v3 dropped framer-motion and @emotion/styled as dependencies (Chakra UI migration guide). App Router setup runs through a CLI that generates a Provider composing ChakraProvider with next-themes for color mode (App Router guide):

npm i @chakra-ui/react @emotion/react
npx @chakra-ui/cli snippet add
// app/layout.tsx (stays a Server Component)
import { Provider } from "@/components/ui/provider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html suppressHydrationWarning>
      <body>
        <Provider>{children}</Provider>
      </body>
    </html>
  );
}

Theming moved from extendTheme() to createSystem(defaultConfig, defineConfig({...})), and useColorMode was removed in favor of next-themes with _dark style props. If Turbopack produces Emotion hydration errors, Chakra's guide suggests adding --webpack to your dev and build scripts.

5. Tailwind CSS with Headless UI

Every step of the classic Tailwind install is obsolete in v4: no npx tailwindcss init, no tailwind.config.js, no content array, no three @tailwind directives (Tailwind upgrade guide). The Next.js setup is three pieces (Tailwind Next.js guide):

npm install tailwindcss @tailwindcss/postcss postcss
// postcss.config.mjs
const config = {
  plugins: { "@tailwindcss/postcss": {} },
};
export default config;
/* app/globals.css */
@import "tailwindcss";

Tailwind v4 detects source files automatically (files in .gitignore are skipped; override with @source) and defines theme tokens in CSS via @theme. Classes must still appear as complete strings in source; runtime string concatenation will not generate CSS.

Headless UI v2 supplies accessible menus, dialogs, and listboxes without styles. It is a client-only package because "most of our components rely on React Context and browser-specific APIs" (GitHub issue #2021), so import it only in 'use client' files. The Epic Next.js tutorial builds a full Strapi frontend on Tailwind v4.

6. Radix UI

Radix UI's maintainers now recommend the single tree-shakeable radix-ui package over per-component @radix-ui/react-* installs (Radix UI introduction):

npm install radix-ui@latest
// components/CustomDropdown.tsx
import { DropdownMenu } from "radix-ui";
import styles from "./DropdownMenu.module.css";

export function CustomDropdown() {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger className={styles.trigger}>Options</DropdownMenu.Trigger>
      <DropdownMenu.Content className={styles.content}>
        <DropdownMenu.Item className={styles.item}>New Tab</DropdownMenu.Item>
        <DropdownMenu.Separator className={styles.separator} />
        <DropdownMenu.Item className={styles.item}>Settings</DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu.Root>
  );
}

Interactive primitives include 'use client' internally, so they drop into App Router pages without a wrapper (Radix releases). If you would rather not style from scratch, shadcn/ui copies styled Radix-based components into your repo; its default style now uses the unified package, and CLI v4 adds a --base flag for Base UI primitives instead (shadcn/ui changelog).

Form Handling and Validation Libraries

Forms in the App Router have two validation points: the client for immediate feedback, and the Server Action for data integrity. One shared Zod schema covers both.

7. React Hook Form

React Hook Form registers uncontrolled fields, which is why it re-renders far less than controlled-form libraries.

// components/NewsletterForm.tsx
'use client';
import { useForm } from 'react-hook-form';

function NewsletterForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const onSubmit = (data) => console.log(data);

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

For Server Actions, RHF's Form component accepts an action prop and submits with progressive enhancement (Form component docs). The Zod and RHF guide walks through the pairing in a TypeScript project.

8. Zod

Zod 4 cut its core from 12.47 kB to 5.36 kB gzipped, added a 1.88 kB Zod Mini build, and parses strings 14.71 times faster than v3 (Zod 4 release notes). String formats moved to top-level functions: z.email() replaces the deprecated z.string().email(). @hookform/resolvers supports Zod 4 from v5.1.0 onward (resolvers PR #777).

// lib/schema.ts
import { z } from 'zod';

export const signupSchema = z.object({
  email: z.email(),
  password: z.string().min(8),
});
export type Signup = z.infer<typeof signupSchema>;

Reuse the schema in a Server Action with safeParse and return field errors to useActionState on the client (Next.js forms guide):

// app/actions/signup.ts
'use server';
import { signupSchema } from './schema';

export async function signup(prevState, formData: FormData) {
  const result = signupSchema.safeParse({
    email: formData.get('email'),
    password: formData.get('password'),
  });
  if (!result.success) {
    return { errors: result.error.flatten().fieldErrors };
  }
  // create the user
}

Animation Library

9. Motion (Formerly Framer Motion)

Framer Motion became Motion for React in November 2024 (Motion announcement). Install motion and import from motion/react; the framer-motion package still receives releases and its import "will work for many versions to come," so migration is not urgent (Motion for React docs).

// components/FadeIn.tsx
'use client';
import { motion } from "motion/react";

export function FadeIn({ children }) {
  return (
    <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }}>
      {children}
    </motion.div>
  );
}

Motion components need 'use client', or you can import from motion/react-client to keep the JavaScript footprint smaller (installation docs). AnimatePresence handles exit animations, with a "wait" mode that queues entering elements until exits finish; keep the conditional inside the wrapper, because an unmounting AnimatePresence cannot animate its children out (AnimatePresence docs). useReducedMotion returns the device's reduced-motion preference. One performance detail: transform shortcuts like { x: 100 } animate through CSS variables and are not hardware accelerated, so animate the transform property directly when frame rate matters (performance docs).

Authentication, Database, and SEO Tooling

10. Auth.js (NextAuth.js)

Auth.js is now part of Better Auth, and its README says: "We recommend new projects to start with Better Auth unless there are some very specific feature gaps" (Auth.js README). v5, the version with App Router support, has stayed at 5.0.0-beta while the npm latest tag still points to v4 (Auth.js installation). Existing apps keep working; maintenance continues for security fixes.

If you stay on Auth.js v5, one auth() call replaces getServerSession, getSession, withAuth, and useSession (migrating to v5):

// auth.ts
import NextAuth from "next-auth";
export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [] });
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;

Next.js 16 renamed middleware.ts to proxy.ts, so the export becomes export { auth as proxy } from "@/auth". Role-based access is not built in; you add jwt and session callbacks that copy a role onto the token (Auth.js RBAC guide).

With Strapi, the usual pattern is a credentials provider that posts to POST /api/auth/local and stores the returned JWT in the session. Strapi 5's Users and Permissions plugin ships this endpoint by default on the free plan and offers a refresh JWT mode with short-lived tokens (Users and Permissions docs). The user authentication guide implements it end to end, and the NextAuth.js guide covers the Auth.js side.

11. Prisma

Prisma 7 replaced its Rust query engine with a TypeScript/WASM compiler, shrinking the bundle about 90% (from roughly 14 MB to 1.6 MB) and opening runtimes like Cloudflare Workers and Vercel Edge (Prisma engineering blog). The cost is a rewrite of every old snippet: prisma-client-js is deprecated in favor of prisma-client with a required output, imports come from the generated folder, and new PrismaClient() without a driver adapter throws (Prisma 7 upgrade guide).

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}
// lib/prisma.ts
import { PrismaClient } from "../prisma/generated/client";

// `adapter` is a Prisma driver adapter instance, for example from @prisma/adapter-pg
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const prisma = globalForPrisma.prisma || new PrismaClient({ adapter });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

Call Prisma only in Server Components and Server Actions, then revalidatePath() after mutations (Prisma API patterns). Prisma manages your application database (orders, profiles, anything not modeled as content); Strapi manages its own content database separately. Drizzle ORM is the main alternative at roughly 16 million weekly downloads; the ORM comparison weighs both.

12. next-seo and the Metadata API

For App Router projects, next-seo's own README directs you to Next.js's built-in generateMetadata for standard <meta> and <title> tags, with next-seo/pages reserved for the Pages Router (next-seo README). What the library still adds on the App Router is typed JSON-LD components for structured data.

Cover title, description, Open Graph, Twitter cards, and canonical URLs with a metadata export or generateMetadata in a Server Component (generateMetadata reference):

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);
  return {
    title: post.title,
    description: post.excerpt,
    alternates: { canonical: `https://example.com/blog/${slug}` },
    openGraph: { title: post.title, description: post.excerpt, images: [post.featuredImage] },
  };
}

params has been async since Next.js 15 (Next.js 15 upgrade guide). Sitemaps use the sitemap.ts file convention, robots rules use robots.ts (robots reference), and OG images use opengraph-image (OG image reference). For JSON-LD, Next.js recommends a plain <script type="application/ld+json"> in the page (JSON-LD guide); next-seo's components save boilerplate when you have many schema types. The Next.js SEO guide covers sourcing these fields from Strapi.

How These Next.js Libraries Fit Together with Strapi

Each layer in a Strapi-backed Next.js app owns a distinct job.

  • Strapi models content in the Content-Type Builder, generates REST endpoints per Content-Type, issues JWTs and API tokens, and fires webhooks on publish. All content types are private until you grant public permissions or authenticate. GraphQL needs the separate @strapi/plugin-graphql install, and its depthLimit and maxLimit default to unlimited, so set both before production (GraphQL plugin docs).
  • Next.js Server Components fetch content, cache it with tags, and expose a Route Handler that Strapi's webhook calls to run revalidateTag(). In Next.js 16 the function takes a cacheLife profile as its second argument; revalidateTag(tag, 'max') gives stale-while-revalidate behavior (revalidateTag reference). Strapi's preview feature expects revalidatePath() on the frontend (Preview docs).
  • TanStack Query or SWR handle client-side polling and optimistic updates against your own API routes.
  • Zustand holds per-request UI state.
  • Prisma owns application data outside the CMS.

Type safety across the boundary comes from generating client types against an OpenAPI description of your Strapi API; the type-safe fetch guide shows the workflow. The REST vs. GraphQL comparison helps with the API choice, and streaming Dynamic Zones covers Suspense-based rendering of flexible layouts.

Building Your Next.js and Strapi Stack

Start with Server Components plus Strapi's REST API, add TanStack Query or SWR only when you need client-side refetching, and commit to one styling camp. The getting started guide wires the first fetch, and the Next.js integration page collects starters and tutorials. If an AI coding agent is part of your workflow, the built-in Strapi MCP server (GA since Strapi 5.49.0, disabled by default, enabled with mcp: { enabled: true } in config/server.ts) lets the agent create, update, and publish content through a /mcp endpoint authenticated with an admin token (Strapi MCP server docs).

Paul BratslavskyDeveloper Advocate

Related Posts

How tos·6 min read

Create a Temperature converter using NextJs, Tailwind CSS and Strapi

In this tutorial you will learn how to build a Temperature Converter with Next.js and Tailwind CSS as frontend and Strapi headless CMS as backend.

·November 4, 2021
Tools & Concepts·7 min read

Advantages of using Next.js + Strapi vs React + Strapi

In this article, we’ll be looking at the advantage of using next.js and strapi over create-react app and strapi

·November 29, 2021
Next (React.js) and Strapi
How tosBeginner·33 min read

Build a blog with Next (React.js) and Strapi

Learn how to build a blog using Next.js (React.js) for the front-end and Strapi as the back-end.

·December 12, 2019