How to manage state in a Next.js app?

How to manage state in a Next.js app?

State management is a critical aspect of building modern web applications. In Next.js, you have multiple options for managing state, each suited for different use cases. Whether you're handling client-side state, server-side state, or global application state, choosing the right approach ensures scalability, performance, and maintainability.

This guide explores the best practices for state management in Next.js, covering:

  1. Client-Side State Management

    • React’s useState and useReducer
    • Context API
    • Zustand
    • Jotai
    • Redux
  2. Server-Side State Management

    • Data Fetching in Next.js (getStaticProps, getServerSideProps)
    • React Query
    • SWR
  3. URL-Based State Management

    • Query Parameters
    • Next.js Router
  4. Choosing the Right State Management Solution

The end of this guide, you’ll understand how to efficiently manage state in Next.js applications while optimizing performance and developer experience.

Client-Side State Management

Client-side state refers to data that persists only in the browser and doesn’t require server interaction. Next.js supports all React state management solutions, so you can choose based on complexity and scalability needs.

React’s Built-in State Hooks (useState and useReducer)

For simple local state, React’s useState is the easiest solution.

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

For more complex state logic, useReducer provides better control:

import { useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>Increment</button>
      <button onClick={() => dispatch({ type: "decrement" })}>Decrement</button>
    </div>
  );
}

When to use:

  • Simple component-level state (useState)
  • Complex state transitions (useReducer)

Context API for Global State

The Context API is useful for sharing state across multiple components without prop drilling.

import { createContext, useContext, useState } from "react";

const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}

Wrap your app with ThemeProvider in _app.js:

import { ThemeProvider } from "../context/ThemeContext";

export default function App({ Component, pageProps }) {
  return (
    <ThemeProvider>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

When to use:

  • Medium-sized apps with limited global state
  • Avoiding prop drilling

Zustand for Lightweight State Management

Zustand is a minimal state management library that avoids boilerplate.

import create from "zustand";

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));

export default function Counter() {
  const { count, increment, decrement } = useStore();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

When to use:

  • Lightweight global state without Context API overhead

Jotai for Atomic State

Jotai provides atomic state management, ideal for fine-grained reactivity.

import { atom, useAtom } from "jotai";

const countAtom = atom(0);

export default function Counter() {
  const [count, setCount] = useAtom(countAtom);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

When to use:

  • Optimized re-renders with atomic updates

Redux for Large-Scale Applications

Redux remains a robust solution for complex state management.

import { Provider, useSelector, useDispatch } from "react-redux";
import { configureStore, createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
});

const store = configureStore({ reducer: counterSlice.reducer });

export default function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(counterSlice.actions.increment())}>
        Increment
      </button>
    </div>
  );
}

When to use:

  • Large applications with complex state logic
  • Middleware requirements (e.g., Redux Thunk, Redux Saga)

Server-Side State Management

Next.js enables server-side rendering (SSR), static site generation (SSG), and incremental static regeneration (ISR). Managing server-side state efficiently ensures fast page loads and SEO benefits.

Data Fetching in Next.js (getStaticProps, getServerSideProps)

For static and server-rendered pages, use:

export async function getStaticProps() {
  const res = await fetch("https://api.example.com/data");
  const data = await res.json();

  return { props: { data } };
}

export default function Page({ data }) {
  return <div>{data.title}</div>;
}

When to use:

  • Pre-rendering pages at build time (getStaticProps)
  • Server-side rendering per request (getServerSideProps)

React Query for Server-State Synchronization

React Query simplifies fetching, caching, and updating server state.

import { useQuery } from "@tanstack/react-query";

async function fetchPosts() {
  const res = await fetch("https://api.example.com/posts");
  return res.json();
}

export default function Posts() {
  const { data, isLoading, error } = useQuery(["posts"], fetchPosts);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

When to use:

  • Managing asynchronous server state
  • Background data refetching and caching

SWR for Data Fetching

Vercel’s SWR is another lightweight data-fetching library.

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function Profile() {
  const { data, error } = useSWR("/api/user", fetcher);

  if (error) return <div>Failed to load</div>;
  if (!data) return <div>Loading...</div>;

  return <div>Hello, {data.name}!</div>;
}

When to use:

  • Fast, lightweight data fetching
  • Built-in caching and revalidation

URL-Based State Management

For state that should persist across page reloads or be shareable via URL, use query parameters.

Using Next.js Router for Query Parameters

import { useRouter } from "next/router";

export default function Search() {
  const router = useRouter();
  const { query } = router;

  const handleSearch = (term) => {
    router.push({ query: { ...query, search: term } });
  };

  return (
    <input
      type="text"
      defaultValue={query.search || ""}
      onChange={(e) => handleSearch(e.target.value)}
    />
  );
}

When to use:

  • Preserving state in the URL (e.g., search filters)

Choosing the Right State Management Solution

Use CaseRecommended Solution
Local component stateuseState, useReducer
Global client stateContext API, Zustand, Jotai
Large-scale applicationsRedux
Server-side data fetchinggetStaticProps, getServerSideProps
Async server stateReact Query, SWR
URL-persisted stateNext.js Router

Conclusion

Next.js offers multiple state management strategies, each suited for different scenarios. For simple state, React hooks suffice. For global state, Zustand or Context API works well. Redux remains ideal for complex applications. Server-side state benefits from React Query or SWR, while URL-based state is best managed with Next.js Router.

By selecting the right approach, you ensure a performant, maintainable, and scalable Next.js application. Evaluate your project’s needs and choose accordingly.

Now that you understand state management in Next.js, implement these techniques to build robust, efficient applications. Happy coding! 🚀

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 to integrate Next.js with a REST API?

How to integrate Next.js with a REST API?

Next.js is a React framework that enables developers to build server-rendered applications easily, enhancing user experiences with minimal effort. On

Dive Deeper
How to handle authentication in Next.js applications?

How to handle authentication in Next.js applications?

Authentication is a crucial aspect of web development, and Next.js, as a popular React framework, provides robust tools and methods to implement secu

Dive Deeper
What is API routes in Next.js and how to use them?

What is API routes in Next.js and how to use them?

In the ever-evolving landscape of web development, Next.js has emerged as a powerful framework for building server-side rendered React applications.

Dive Deeper
How to add SEO tags to the Next.js website?

How to add SEO tags to the Next.js website?

When it comes to optimizing your Next.js website, SEO tags are your best friend. These tags help search engines understand your site's content, impro

Dive Deeper
How to migrate a React app to Next.js?

How to migrate a React app to Next.js?

React is a popular JavaScript library for building user interfaces, while Next.js is a React framework that adds server-side rendering, static site g

Dive Deeper