This article will teach you how to build a full-stack CRUD App with Next.js, React Query, GraphQL Code Generator, React-Hook-Form, Zod, and graphql-request to perform Create/Update/Get/Delete operations.

Next.js, React Query, and GraphQL Series:

  1. GraphQL API with Next.js & MongoDB: Access & Refresh Tokens
  2. GraphQL CRUD API with Next.js, MongoDB, and TypeGraphQL
  3. Next.js, GraphQL-CodeGen, & React Query: JWT Authentication
  4. Next.js Full-Stack App with React Query, and GraphQL-CodeGen

More practice:

Next.js Full-Stack App with React Query, and GraphQL-CodeGen

Next.js Full-Stack CRUD App Overview

We will build a Next.js, tailwindCss, TypeScript, React-Hook-Form, Zod client with React Query, and graphql-request to make CRUD operations against a GraphQL API.

-On the homepage, a React Query GraphQL request is made to the Next.js GraphQL server to retrieve all the posts.

nextjs react query graphql codegen fullstack app get all posts

-To add a new post to the database, click on the “Create Post” link from the navigation menu to display the create post modal.

Next, provide the necessary information and make a React Query GraphQL mutation request to the Next.js GraphQL server to add the post to the database.

nextjs react query graphql codegen fullstack app create post

-To update a post in the database, click on the three dots on the post to display the update post modal.

nextjs react query graphql codegen fullstack app edit post button

Next, edit the fields and make a React Query GraphQL mutation request to the Next.js GraphQL API to update that specific post in the database.

nextjs react query graphql codegen fullstack app update post

-To remove a post from the database, click on the three dots again and you should be prompted to confirm your action before a React Query GraphQL mutation request is made to the Next.js GraphQL API to remove that particular post from the database.

nextjs react query graphql codegen fullstack app delete post

Benefits of React Query

React Query is a powerful asynchronous server state management library for ReactJs/NextJs. In layman’s terms, it makes fetching, caching, cache synchronizing, and updating server state a breeze in React.js/Next.js applications.

React Query is now an adaptor categorized under TanStackQuery upon the release of version 4.

There are other TanStackQuery adaptors like:

I know there are other server state management libraries like SWR, Apollo Client, and RTK Query but when you carefully analyze the benchmark for the popular server state libraries on the TanStackQuery website, you will notice that React Query outperforms its competitors.

React Query uses a fetching mechanism that is agnostically built on Promises, which makes it compatible with any asynchronous data fetching clients like GraphQL-Request, Axios, FetchAPI, and many more.

Setup GraphQL Code Generator

GraphQL Code Generator is a toolkit tailored to simplify and automate the generation of typed queries, subscriptions, and mutations for React, Next.js, Vue, Angular, Svelte, and other supported frontend frameworks.

To begin, let’s install the GraphQL Code Generator CLI tool with this command:


yarn add -D graphql @graphql-codegen/cli
# or 
npm install -D graphql @graphql-codegen/cli

There are two ways to get the GraphQL CodeGen up and running:

  • Initialization Wizard – guides you through the whole process of setting up a schema, choosing and installing the required plugins, picking a destination to output the generated files, and many more.
  • Manual Setup – gives you the freedom to install plugins and configure them yourself.

However, we will be using the manual process to help you understand what happens under the hood.

Now since we are working with React Query, let’s install the required plugins provided by GraphQL Code Generator:


yarn add -D @graphql-codegen/typescript-operations @graphql-codegen/typescript @graphql-codegen/typescript-react-query
# or 
npm install -D @graphql-codegen/typescript-operations @graphql-codegen/typescript @graphql-codegen/typescript-react-query

Quite a lot of plugins, let me explain the purpose of each plugin:

  • @graphql-codegen/typescript-operations – this plugin generates the TypeScript types for the Queries, Mutations, Subscriptions, and Fragments that are only in use.
  • @graphql-codegen/typescript – this plugin generates the base TypeScript types, depending on the structure of the GraphQL schema.
  • @graphql-codegen/typescript-react-query – this plugin generates typed hooks for the various GraphQL operations.

Out-of-the-box, the GraphQL CodeGen CLI relies on a configuration file, codegen.yml , codegen.js or codegen.json to manage all possible options.

In the root directory, create a codegen.yml file and add the following configurations needed by the GraphQL CodeGen CLI.

codegen.yml


schema: http://localhost:3000/api/graphql
documents: './client/**/*.graphql'
generates:
  ./client/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-query
    config:
      fetcher: graphql-request

  • schema – the path to a schema file or the URL of a GraphQL endpoint.
  • documents – an array of paths indicating the locations of the GraphQL files.
  • generates – indicates the destination to output the generated code.
  • plugins – list the plugins needed by CodeGen
  • fetcher – the asynchronous data fetching client

Now add the script below to the package.json file.

package.json


{
"scripts": {
    "generate": "graphql-codegen --config codegen.yml"
  }
}

Creating the GraphQL Mutations and Queries

Now that we’ve configured GraphQL Code Generator, let’s add the queries and mutations to the client/graphql folder.

Create Post Mutation

client/graphql/CreatePostMutation.graphql


mutation CreatePost($input: PostInput!) {
  createPost(input: $input) {
    status
    post {
      id
      title
      content
      category
      user
      image
      createdAt
      updatedAt
    }
  }
}

Update Post Mutation

client/graphql/UpdatePostMutation.graphql


mutation UpdatePost($input: UpdatePostInput!, $updatePostId: String!) {
  updatePost(input: $input, id: $updatePostId) {
    status
    post {
      id
      title
      content
      category
      image
      createdAt
      updatedAt
    }
  }
}

Delete Post Mutation

client/graphql/DeletePostMutation.graphql


mutation DeletePost($deletePostId: String!) {
  deletePost(id: $deletePostId)
}

Get a Single Post Query

client/graphql/GetPostQuery.graphql


query GetAllPosts($input: PostFilter!) {
  getPosts(input: $input) {
    status
    results
    posts {
      id
      _id
      id
      title
      content
      category
      user {
        email
        name
        photo
      }
      image
      createdAt
      updatedAt
    }
  }
}

Get All Post Query

client/graphql/GetAllPostsQuery.graphql


query GetAllPosts($input: PostFilter!) {
  getPosts(input: $input) {
    status
    results
    posts {
      id
      _id
      id
      title
      content
      category
      user {
        email
        name
        photo
      }
      image
      createdAt
      updatedAt
    }
  }
}

Generating the React Query Hooks with CodeGen

Since we have defined the mutations and queries, let’s execute the generate script we included in the package.json file.


yarn generate
# or 
npm run generate

After CodeGen has generated the code, you should see a newly-created ./client/generated/graphql.ts file having generated TypesScript types, and React Query hooks.

Create Reusable Components with tailwindCss

Creating the Modal Component

Modals are very useful for collecting user information, providing updates, or encouraging users to take specific actions.

We are going to use React Portals to display the modal. React Portals allow us to render a component outside the DOM hierarchy of the parent component to avoid compromising the parent-child relationship between components.

Now let’s use the createPortal function provided by react-dom in conjunction with tailwindCss to create a reusable modal component.

client/components/modals/post.modal.tsx


import ReactDom from 'react-dom';
import React, { FC } from 'react';

type IPostModal = {
  openPostModal: boolean;
  setOpenPostModal: (openPostModal: boolean) => void;
  children: React.ReactNode;
};

const PostModal: FC<IPostModal> = ({
  openPostModal,
  setOpenPostModal,
  children,
}) => {
  if (!openPostModal) return null;
  return ReactDom.createPortal(
    <>
      <div
        className='fixed inset-0 bg-[rgba(0,0,0,.5)] z-[1000]'
        onClick={() => setOpenPostModal(false)}
      ></div>
      <div className='max-w-lg w-full rounded-md fixed top-[15%] left-1/2 -translate-x-1/2 bg-white z-[1001] p-6'>
        {children}
      </div>
    </>,
    document.getElementById('post-modal') as HTMLElement
  );
};

export default PostModal;

Next, let’s create a Next.js _document.tsx page and add a Div with a post-modal ID attribute. This will make React to render the modal outside the DOM hierarchy but within the <div id='post-modal'></div> element.

pages/_document.tsx


import { Html, Head, Main, NextScript } from 'next/document';

export default function Document() {
  return (
    <Html>
      <Head>
        <link
          href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css'
          rel='stylesheet'
        ></link>
      </Head>
      <body className='font-Poppins'>
        <Main />
        <NextScript />
        <div id='post-modal'></div>
      </body>
    </Html>
  );
}

Creating the Message Component

client/components/Message.tsx


import React, { FC } from 'react';

type IMessageProps = {
  children: React.ReactNode;
};
const Message: FC<IMessageProps> = ({ children }) => {
  return (
    <div
      className='max-w-3xl mx-auto rounded-lg px-4 py-3 shadow-md bg-teal-100 flex items-center justify-center h-40'
      role='alert'
    >
      <span className='text-teal-500 text-xl font-semibold'>{children}</span>
    </div>
  );
};

export default Message;

Creating a Custom Input Field with React-Hook-Form

client/components/TextInput.tsx


import React from 'react';
import { useFormContext } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';

type TextInputProps = {
  label: string;
  name: string;
  type?: string;
};

const TextInput: React.FC<TextInputProps> = ({
  label,
  name,
  type = 'text',
}) => {
  const {
    register,
    formState: { errors },
  } = useFormContext();
  return (
    <div className='mb-2'>
      <label className='block text-gray-700 text-lg mb-2' htmlFor='title'>
        {label}
      </label>
      <input
        className={twMerge(
          `appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none`,
          `${errors[name] && 'border-red-500'}`
        )}
        type={type}
        {...register(name)}
      />
      <p
        className={twMerge(
          `text-red-500 text-xs italic mb-2 invisible`,
          `${errors[name] && 'visible'}`
        )}
      >
        {errors[name]?.message as string}
      </p>
    </div>
  );
};

export default TextInput;

GraphQL Request and React Query Clients

Create a client/requests/graphqlRequestClient.ts file and add the following code to create the React Query and GraphQL request clients.

client/requests/graphqlRequestClient.ts


import { GraphQLClient } from 'graphql-request';
import { QueryClient } from 'react-query';

const GRAPHQL_ENDPOINT = process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT as string;

const graphqlRequestClient = new GraphQLClient(GRAPHQL_ENDPOINT, {
  credentials: 'include',
  mode: 'cors',
});

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 1000,
    },
  },
});

export default graphqlRequestClient;

React Query & GraphQL Request Create Mutation

The CreatePost component contains a form built with the React-Hook-Form library that contains the fields required to create a new post.

The form validation rules are defined with the Zod schema validation library and passed to the React-Hook-Form useForm() method via the zodResolver() function. You can read more about the Zod library from https://github.com/colinhacks/zod.

The useForm() hook function provided by React-Hook-Form returns an object containing methods and properties from handling the form submission to displaying the errors.

client/components/posts/create.post.tsx


import React, { FC, useEffect } from 'react';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import { object, string, TypeOf } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import FileUpLoader from '../FileUpload';
import { LoadingButton } from '../LoadingButton';
import TextInput from '../TextInput';
import { useCreatePostMutation } from '../../generated/graphql';
import graphqlRequestClient, {
  queryClient,
} from '../../requests/graphqlRequestClient';
import { toast } from 'react-toastify';
import useStore from '../../store';

const createPostSchema = object({
  title: string().min(1, 'Title is required'),
  category: string().min(1, 'Category is required'),
  content: string().min(1, 'Content is required'),
  image: string().min(1, 'Image is required'),
});

type CreatePostInput = TypeOf<typeof createPostSchema>;

type ICreatePostProp = {
  setOpenPostModal: (openPostModal: boolean) => void;
};

const CreatePost: FC<ICreatePostProp> = ({ setOpenPostModal }) => {
  const store = useStore();
  const { isLoading, mutate: createPost } = useCreatePostMutation(
    graphqlRequestClient,
    {
      onSuccess(data) {
        store.setPageLoading(false);
        setOpenPostModal(false);
        queryClient.refetchQueries('GetAllPosts');
        toast('Post created successfully', {
          type: 'success',
          position: 'top-right',
        });
      },
      onError(error: any) {
        store.setPageLoading(false);
        setOpenPostModal(false);
        error.response.errors.forEach((err: any) => {
          toast(err.message, {
            type: 'error',
            position: 'top-right',
          });
        });
      },
    }
  );
  const methods = useForm<CreatePostInput>({
    resolver: zodResolver(createPostSchema),
  });

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = methods;

  useEffect(() => {
    if (isLoading) {
      store.setPageLoading(true);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isLoading]);

  const onSubmitHandler: SubmitHandler<CreatePostInput> = async (data) => {
    createPost({ input: data });
  };
  return (
    <section>
      <h2 className='text-2xl font-semibold mb-4'>Create Post</h2>

      <FormProvider {...methods}>
        <form className='w-full' onSubmit={handleSubmit(onSubmitHandler)}>
          <TextInput name='title' label='Title' />
          <TextInput name='category' label='Category' />
          <div className='mb-2'>
            <label className='block text-gray-700 text-lg mb-2' htmlFor='title'>
              Content
            </label>
            <textarea
              className={twMerge(
                `appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none`,
                `${errors.content && 'border-red-500'}`
              )}
              rows={4}
              {...register('content')}
            />
            <p
              className={twMerge(
                `text-red-500 text-xs italic mb-2 invisible`,
                `${errors.content && 'visible'}`
              )}
            >
              {errors.content ? errors.content.message : ''}
            </p>
          </div>
          <FileUpLoader name='image' />
          <LoadingButton loading={isLoading} textColor='text-ct-blue-600'>
            Create Post
          </LoadingButton>
        </form>
      </FormProvider>
    </section>
  );
};

export default CreatePost;

In the above, we evoked the useCreatePostMutation() hook generated by the GraphQL Code Generator and provided it with the GraphQL client we defined in the client/requests/graphqlRequestClient.ts file.

Also, the React-Hook-Form handleSubmit() function is evoked when the form is submitted. If the form is valid the create post mutation will make a request to the Next.js GraphQL API to add the new post to the database.

If the mutation resolves successfully, the queryClient.refetchQueries('GetAllPosts') will be evoked to re-fetch all the posts from the database.

On the other hand, if the mutation resolves in error, the React-Toastify component will be evoked to display them.

React Query & GraphQL Request Update Mutation

The UpdatePost component is similar to the CreatePost component with some little tweaks.

Here we will evoke the useUpdatePostMutation() hook generated by GraphQL Code Generator to update the post in the database.

client/components/posts/update.post.tsx


import React, { FC, useEffect } from 'react';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import { object, string, TypeOf } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import FileUpLoader from '../FileUpload';
import { LoadingButton } from '../LoadingButton';
import TextInput from '../TextInput';
import { useUpdatePostMutation } from '../../generated/graphql';
import graphqlRequestClient from '../../requests/graphqlRequestClient';
import { toast } from 'react-toastify';
import useStore from '../../store';
import { IPost } from '../../lib/types';
import { useQueryClient } from 'react-query';

type IUpdatePostProps = {
  post: IPost;
  setOpenPostModal: (openPostModal: boolean) => void;
};

const updatePostSchema = object({
  title: string().min(1, 'Title is required'),
  category: string().min(1, 'Category is required'),
  content: string().min(1, 'Content is required'),
  image: string().min(1, 'Image is required'),
});

type UpdatePostInput = TypeOf<typeof updatePostSchema>;

const UpdatePost: FC<IUpdatePostProps> = ({ post, setOpenPostModal }) => {
  const store = useStore();
  const queryClient = useQueryClient();
  const { isLoading, mutate: updatePost } = useUpdatePostMutation(
    graphqlRequestClient,
    {
      onSuccess(data) {
        store.setPageLoading(false);
        setOpenPostModal(false);
        queryClient.refetchQueries('GetAllPosts');
        toast('Post updated successfully', {
          type: 'success',
          position: 'top-right',
        });
      },
      onError(error: any) {
        store.setPageLoading(false);
        setOpenPostModal(false);
        error.response.errors.forEach((err: any) => {
          toast(err.message, {
            type: 'error',
            position: 'top-right',
          });
        });
      },
    }
  );
  const methods = useForm<UpdatePostInput>({
    resolver: zodResolver(updatePostSchema),
  });

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = methods;

  useEffect(() => {
    if (isLoading) {
      store.setPageLoading(true);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isLoading]);

  useEffect(() => {
    if (post) {
      methods.reset(post);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const onSubmitHandler: SubmitHandler<UpdatePostInput> = async (data) => {
    updatePost({ input: data, updatePostId: post._id });
  };
  return (
    <section>
      <h2 className='text-2xl font-semibold mb-4'>Update Post</h2>
      <FormProvider {...methods}>
        <form className='w-full' onSubmit={handleSubmit(onSubmitHandler)}>
          <TextInput name='title' label='Title' />
          <TextInput name='category' label='Category' />
          <div className='mb-2'>
            <label className='block text-gray-700 text-lg mb-2' htmlFor='title'>
              Content
            </label>
            <textarea
              className={twMerge(
                `appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none`,
                `${errors.content && 'border-red-500'}`
              )}
              rows={4}
              {...register('content')}
            />
            <p
              className={twMerge(
                `text-red-500 text-xs italic mb-2 invisible`,
                `${errors.content && 'visible'}`
              )}
            >
              {errors.content ? errors.content.message : ''}
            </p>
          </div>
          <FileUpLoader name='image' />
          <LoadingButton loading={isLoading} textColor='text-ct-blue-600'>
            Update Post
          </LoadingButton>
        </form>
      </FormProvider>
    </section>
  );
};

export default UpdatePost;

React Query & GraphQL Request Delete Mutation

client/components/posts/post.component.tsx


import React, { FC, useEffect, useState } from 'react';
import { format, parseISO } from 'date-fns';
import { twMerge } from 'tailwind-merge';
import Image from 'next/future/image';
import { IPost } from '../../lib/types';
import { useDeletePostMutation } from '../../generated/graphql';
import graphqlRequestClient, {
  queryClient,
} from '../../requests/graphqlRequestClient';
import { toast } from 'react-toastify';
import useStore from '../../store';
import PostModal from '../modals/post.modal';
import UpdatePost from './update.post';

type PostItemProps = {
  post: IPost;
};

const PostItem: FC<PostItemProps> = ({ post }) => {
  const [openMenu, setOpenMenu] = useState(false);
  const [openPostModal, setOpenPostModal] = useState(false);

  const store = useStore();
  const { isLoading, mutate: deletePost } = useDeletePostMutation(
    graphqlRequestClient,
    {
      onSuccess(data) {
        store.setPageLoading(false);
        queryClient.refetchQueries('GetAllPosts');
        toast('Post deleted successfully', {
          type: 'success',
          position: 'top-right',
        });
      },
      onError(error: any) {
        store.setPageLoading(false);
        error.response.errors.forEach((err: any) => {
          toast(err.message, {
            type: 'error',
            position: 'top-right',
          });
        });
      },
    }
  );

  useEffect(() => {
    if (isLoading) {
      store.setPageLoading(true);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isLoading]);

  const toggleMenu = () => {
    setOpenMenu(!openMenu);
  };

  const onDeleteHandler = (id: string) => {
    toggleMenu();
    if (window.confirm('Are you sure')) {
      deletePost({ deletePostId: id });
    }
  };
  return (
    <>
      <div className='rounded-md shadow-md bg-white'>
        <div className='mx-2 mt-2 overflow-hidden rounded-md'>
          <Image
            src={post.image}
            alt={post.title}
            className='object-fill w-full h-full'
            width={400}
            height={250}
          />
        </div>
        <div className='p-4'>
          <h5 className='font-semibold text-xl text-[#4d4d4d] mb-4'>
            {post.title.length > 25
              ? post.title.substring(0, 25) + '...'
              : post.title}
          </h5>
          <div className='flex items-center mt-4'>
            <p className='p-1 rounded-sm mr-4 bg-[#dad8d8]'>{post.category}</p>
            <p className='text-[#ffa238]'>
              {format(parseISO(post.createdAt), 'PPP')}
            </p>
          </div>
        </div>
        <div className='flex justify-between items-center px-4 pb-4'>
          <div className='flex items-center'>
            <div className='w-12 h-12 rounded-full overflow-hidden'>
              <Image
                src={post.user.photo}
                alt={post.user.name}
                className='object-cover w-full h-full'
                height={100}
                width={100}
              />
            </div>
            <p className='ml-4 text-sm font-semibold'>{post.user.name}</p>
          </div>
          <div className='relative'>
            <div
              className='text-3xl text-[#4d4d4d] cursor-pointer p-3'
              onClick={toggleMenu}
            >
              <i className='bx bx-dots-horizontal-rounded'></i>
            </div>
            <ul
              className={twMerge(
                `absolute bottom-5 -right-1 z-50 py-2 rounded-sm bg-white shadow-lg transition ease-out duration-300 invisible`,
                `${openMenu ? 'visible' : 'invisible'}`
              )}
            >
              <li
                className='w-24 h-7 py-3 px-2 hover:bg-[#f5f5f5] flex items-center gap-2 cursor-pointer transition ease-in duration-300'
                onClick={() => {
                  setOpenPostModal(true);
                  toggleMenu();
                }}
              >
                <i className='bx bx-edit-alt'></i> <span>Edit</span>
              </li>
              <li
                className='w-24 h-7 py-3 px-2 hover:bg-[#f5f5f5] flex items-center gap-2 cursor-pointer transition ease-in duration-300'
                onClick={() => onDeleteHandler(post._id)}
              >
                <i className='bx bx-trash'></i> <span>Delete</span>
              </li>
            </ul>
          </div>
        </div>
      </div>
      <PostModal
        openPostModal={openPostModal}
        setOpenPostModal={setOpenPostModal}
      >
        <UpdatePost post={post} setOpenPostModal={setOpenPostModal} />
      </PostModal>
    </>
  );
};

export default PostItem;

React Query & GraphQL Request Get Query

pages/index.tsx


import type { GetServerSideProps, NextPage } from 'next';
import { useEffect } from 'react';
import { dehydrate } from 'react-query';
import { toast } from 'react-toastify';
import Header from '../client/components/Header';
import Message from '../client/components/Message';
import PostItem from '../client/components/posts/post.component';
import {
  GetMeDocument,
  useGetAllPostsQuery,
} from '../client/generated/graphql';
import { axiosGetMe } from '../client/requests/axiosClient';
import graphqlRequestClient, {
  queryClient,
} from '../client/requests/graphqlRequestClient';
import useStore from '../client/store';

export const getServerSideProps: GetServerSideProps = async ({ req }) => {
  if (req.cookies.access_token) {
    await queryClient.prefetchQuery(['getMe', {}], () =>
      axiosGetMe(GetMeDocument, req.cookies.access_token as string)
    );
  } else {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    };
  }
  return {
    props: {
      dehydratedState: dehydrate(queryClient),
      requireAuth: true,
      enableAuth: true,
    },
  };
};

const HomePage: NextPage = () => {
  const store = useStore();
  const { data: posts, isLoading } = useGetAllPostsQuery(
    graphqlRequestClient,
    {
      input: { limit: 10, page: 1 },
    },
    {
      select: (data) => data.getPosts.posts,
      onError(error: any) {
        store.setPageLoading(false);
        error.response.errors.forEach((err: any) => {
          toast(err.message, {
            type: 'error',
            position: 'top-right',
          });
        });
      },
    }
  );

  useEffect(() => {
    if (isLoading) {
      store.setPageLoading(true);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isLoading]);
  return (
    <>
      <Header />
      <section className='bg-ct-blue-600 min-h-screen py-12'>
        <div>
          {posts?.length === 0 ? (
            <Message>There are no posts at the moment</Message>
          ) : (
            <div className='max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-5 px-6'>
              {posts?.map((post) => (
                <PostItem key={post._id} post={post} />
              ))}
            </div>
          )}
        </div>
      </section>
    </>
  );
};

export default HomePage;

Conclusion

With this Next.js, React Query, GraphQL-Request, GraphQL Code Generator, tailwindCss, and React-Hook-Form, Zod example in TypeScript, you’ve learned how to create a full-stack CRUD app with Next.js.

Next.js Full-Stack CRUD App Source Code

Check out the complete source code for: