preloader
If you want to request blogpost topics, feel free to inbox my instagram @adabjawa. Thankyou!
The Most Effective and Simple Way to Implement Redux Toolkit Query in Next.js with CRUD Operations

The Most Effective and Simple Way to Implement Redux Toolkit Query in Next.js with CRUD Operations

When building modern web applications, efficiency and simplicity are crucial. Redux Toolkit Query (RTK Query) simplifies data fetching and caching, making it an excellent choice for managing server state. Coupled with Next.js’s App Router and Tailwind CSS, you can create a scalable, well-styled application with minimal effort.

In this blog post, we’ll demonstrate how to implement RTK Query in a Next.js project using the App Router, perform CRUD operations with the FakeStore API, and style the application with Tailwind CSS. This guide is designed to be practical, modular, and easy to follow, ensuring clean and maintainable code.

Background

Next.js is a powerful React framework that supports server-side rendering, static site generation, and more. Redux Toolkit Query is part of Redux Toolkit, designed to simplify data fetching and caching. Tailwind CSS is a utility-first CSS framework that makes it easy to create responsive and customizable designs.

The FakeStore API provides a straightforward way to simulate interactions with a store, making it ideal for our CRUD operations.

Step-by-Step Guide

1. Setting Up the Project

First, set up a Next.js project with JavaScript and install the necessary dependencies:

npx create-next-app my-app
cd my-app
npm install @reduxjs/toolkit react-redux daisyui
npm install tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p

Configure Tailwind CSS by editing tailwind.config.js:

module.exports = {
  content: [
    './app/**/*.{js,jsx}',
    './components/**/*.{js,jsx}',
    './pages/**/*.{js,jsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [require('daisyui')],
};

Add Tailwind CSS to styles/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

2. Setting Up Redux Toolkit Query

Create an API slice using RTK Query. This will serve as our interface to the FakeStore API.

Create app/store/api.js:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const fakeStoreApi = createApi({
  reducerPath: 'fakeStoreApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://fakestoreapi.com' }),
  endpoints: (builder) => ({
    getProducts: builder.query({
      query: () => '/products',
    }),
    getProductById: builder.query({
      query: (id) => `/products/${id}`,
    }),
    addProduct: builder.mutation({
      query: (product) => ({
        url: '/products',
        method: 'POST',
        body: product,
      }),
    }),
    updateProduct: builder.mutation({
      query: ({ id, ...product }) => ({
        url: `/products/${id}`,
        method: 'PUT',
        body: product,
      }),
    }),
    deleteProduct: builder.mutation({
      query: (id) => ({
        url: `/products/${id}`,
        method: 'DELETE',
      }),
    }),
  }),
});

export const {
  useGetProductsQuery,
  useGetProductByIdQuery,
  useAddProductMutation,
  useUpdateProductMutation,
  useDeleteProductMutation,
} = fakeStoreApi;

3. Setting Up Redux Store

Create the Redux store and configure it in app/store/index.js:

import { configureStore } from '@reduxjs/toolkit';
import { fakeStoreApi } from './api';

export const store = configureStore({
  reducer: {
    [fakeStoreApi.reducerPath]: fakeStoreApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(fakeStoreApi.middleware),
});

Provide the store in app/layout.js:

import { Provider } from 'react-redux';
import { store } from './store';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head />
      <body>
        <Provider store={store}>{children}</Provider>
      </body>
    </html>
  );
}

4. Implementing CRUD Pages

Create pages for listing, viewing, adding, and editing products.

  • List Products (app/products/page.js):
import { useGetProductsQuery } from '../store/api';
import Link from 'next/link';

export default function ProductsPage() {
  const { data, error, isLoading } = useGetProductsQuery();

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error loading products</p>;

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">Products</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
        {data?.map((product) => (
          <div key={product.id} className="border p-4 rounded">
            <h2 className="text-lg font-semibold">{product.title}</h2>
            <p>${product.price}</p>
            <Link href={`/products/${product.id}`}>
              <button className="mt-2 bg-blue-500 text-white py-1 px-2 rounded">View</button>
            </Link>
          </div>
        ))}
      </div>
    </div>
  );
}
  • Product Details (app/products/[id]/page.js):
import { useGetProductByIdQuery } from '../../store/api';
import { useRouter } from 'next/router';

export default function ProductDetailPage({ params }) {
  const { id } = params;
  const { data, error, isLoading } = useGetProductByIdQuery(id);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error loading product</p>;

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold">{data?.title}</h1>
      <p className="mt-2">{data?.description}</p>
      <p className="mt-2 text-lg font-semibold">${data?.price}</p>
    </div>
  );
}
  • Add Product (app/products/add/page.js):
import { useState } from 'react';
import { useAddProductMutation } from '../../store/api';

export default function AddProductPage() {
  const [addProduct] = useAddProductMutation();
  const [formData, setFormData] = useState({
    title: '',
    price: '',
    description: '',
    image: '',
    category: '',
  });

  const handleChange = (e) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    await addProduct(formData);
  };

  return (
    <form onSubmit={handleSubmit} className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">Add New Product</h1>
      <input
        name="title"
        value={formData.title}
        onChange={handleChange}
        placeholder="Title"
        className="border p-2 w-full mb-4"
      />
      {/* Repeat for other fields */}
      <button type="submit" className="bg-green-500 text-white py-2 px-4 rounded">
        Submit
      </button>
    </form>
  );
}
  • Edit Product (app/products/[id]/edit/page.js):
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useGetProductByIdQuery, useUpdateProductMutation } from '../../store/api';

export default function EditProductPage({ params }) {
  const { id } = params;
  const { data, error, isLoading } = useGetProductByIdQuery(id);
  const [updateProduct] = useUpdateProductMutation();
  const [formData, setFormData] = useState({
    title: '',
    price: '',
    description: '',
    image: '',
    category: '',
  });

  useEffect(() => {
    if (data) {
      setFormData(data);
    }
  }, [data]);

  const handleChange = (e) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    await updateProduct({ id, ...formData });
  };

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error loading product</p>;

  return (
    <form onSubmit={handleSubmit} className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">Edit Product</h1>
      <input
        name="title"
        value={formData.title}
        onChange={handleChange}
        placeholder

="Title"
        className="border p-2 w-full mb-4"
      />
      {/* Repeat for other fields */}
      <button type="submit" className="bg-blue-500 text-white py-2 px-4 rounded">
        Update
      </button>
    </form>
  );
}
  • Delete Product (Button in ProductDetailPage):

Add a delete button in the product detail page:

import { useDeleteProductMutation } from '../../store/api';

export default function ProductDetailPage({ params }) {
  const { id } = params;
  const { data, error, isLoading } = useGetProductByIdQuery(id);
  const [deleteProduct] = useDeleteProductMutation();

  const handleDelete = async () => {
    await deleteProduct(id);
    // Redirect or show a success message
  };

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error loading product</p>;

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold">{data?.title}</h1>
      <p className="mt-2">{data?.description}</p>
      <p className="mt-2 text-lg font-semibold">${data?.price}</p>
      <button
        onClick={handleDelete}
        className="mt-4 bg-red-500 text-white py-2 px-4 rounded"
      >
        Delete
      </button>
    </div>
  );
}

5. Final Touches and Best Practices

  • Reusability: Create reusable components for forms and buttons to avoid code duplication.
  • Error Handling: Implement robust error handling to improve user experience and provide feedback.
  • Optimistic Updates: Consider using optimistic updates for a smoother user experience when performing mutations.

Conclusion

Integrating Redux Toolkit Query with Next.js using the App Router and combining it with Tailwind CSS provides a powerful and elegant solution for managing server state and building user interfaces. By following this guide, you’ll be able to create a highly efficient and maintainable CRUD application with minimal boilerplate code.

Feel free to experiment with the provided code and customize it further to suit your needs. Happy coding!

comments powered by Disqus

Related Posts

Building a Secure Authentication System in Next.js with JWT and Middleware

Building a Secure Authentication System in Next.js with JWT and Middleware

Introduction: Securing your web application is essential, and building a robust authentication system is a crucial part of this.

Read More
Atomic Design with Next.js: A Step-by-Step Guide

Atomic Design with Next.js: A Step-by-Step Guide

Introduction: What is Atomic Design? Atomic Design is a methodology for creating design systems with a clear, hierarchical structure.

Read More
JavaScript Best Practices for Writing Clean Code

JavaScript Best Practices for Writing Clean Code

In the fast-paced world of web development, writing clean and maintainable code is more important than ever.

Read More