In this article, you’ll learn how to make Get/Post/Patch/Delete requests to a RESTful API with React, Redux Toolkit, and RTK Query.

React, Redux Toolkit, RTK Query & React Hook Form Tutorial Series:

  1. Form Validation with React Hook Form, Material UI, React and TypeScript
  2. How I Setup Redux Toolkit and RTK Query the right way
  3. React, Material UI and React Hook Form: Login and Signup Forms
  4. React, RTK Query, React Hook Form and Material UI – Image Upload
  5. React + Redux Toolkit: JWT Authentication and Authorization
  6. React.js + Redux Toolkit: Refresh Tokens Authentication

Related Articles:

React CRUD example with Redux Toolkit, RTK Query & REST API

Run the Frontend and Backend Apps

To successfully run both the backend and frontend projects on your machine, follow these step-by-step instructions:

  1. Begin by downloading or cloning the project from its GitHub repository: https://github.com/wpcodevo/JWT_Authentication_React. Open the source code in your preferred text editor.
  2. In the integrated terminal of your IDE or text editor, execute the command docker-compose up -d to launch the MongoDB and Redis Docker containers.
  3. Navigate to the backend directory using the command cd ./backend. Run yarn install to install all required dependencies.
  4. Open the backend/src/app.ts file and uncomment the Nodemailer code.
    nodemailer code of the backend api
    Then, run the command yarn start to initiate the Node.js development server. You should see the Nodemailer SMTP credentials printed in the terminal. Copy the values of the ‘user‘ and ‘pass‘ fields and add them to their respective variables in the backend/.env file. Afterward, comment out the Nodemailer code in the backend/src/app.ts file, and save the file to restart the Node.js development server.
  5. In another terminal, move to the frontend directory from the root level using the command cd ./frontend. Run yarn install to install all necessary dependencies. Once the installation is complete, execute yarn dev to start the Vite development server.
  6. Open the application in your browser by visiting http://localhost:3000/ and explore the app’s features. During the registration and password reset process, a Nodemailer link will be printed in the terminal of the backend server that you can click to open the mailbox.
    Nodemailer mailbox web app in the browser
    Note: Do not visit the app using http://127.0.0.1:3000/ to avoid getting CORS errors.

React, Redux Toolkit & RTK Query example Overview

We will build RTK Query endpoints to make CRUD operations against a RESTful API server.

-To add a new post to the database, make a POST request with the form data to the server.

react rtk query redux toolkit crud operation post request

-To retrieve all the posts from the database, make a GET request to the server.

react rtk query redux toolkit crud app get request

Now to edit the post, hover over the three dots and click on the edit button to open the edit modal.

react rtk query redux toolkit crud app edit post

Next, issue a PATCH request with the form data to update the post in the database.

react rtk query redux toolkit crud app patch request

-To delete a post, hover over the three dots and click on the delete button. After confirming, a DELETE request will be made to the server to remove that post from the database.

react rtk query redux toolkit crud app delete request

The React RTK Query works with the following API endpoints:

HTTP METHODROUTEDESCRIPTION
GET/api/postsRetrieve all posts
POST/api/postsCreates a new post
GET/api/posts/:idReturns a single post
PATCH/api/posts/:idUpdates a post
DELETE/api/posts/:idDeletes a post

You can follow one of these tutorials to build the RESTful API:

How to Set up Redux Toolkit and RTK Query with React

Follow this tutorial How I Setup Redux Toolkit and RTK Query the right way to add Redux Toolkit and RTK Query to your React project.

Project Overview:

react rtk query redux toolkit crud app project setup

Create a Custom Fetch Base in RTK Query

Create a custom baseQuery to wrap around the fetchBaseQuery such that when a 401 Unauthorized error is returned by the server, an additional request will be made in the background to refresh the JWT access token before re-trying the initial query or mutation.

We also need the async-mutex package to prevent multiple requests to refresh the access token when the first attempt fails.

src/redux/api/customFetchBase.ts


import {
  BaseQueryFn,
  FetchArgs,
  fetchBaseQuery,
  FetchBaseQueryError,
} from '@reduxjs/toolkit/query';
import { Mutex } from 'async-mutex';
import { logout } from '../features/userSlice';

const baseUrl = `${process.env.REACT_APP_SERVER_ENDPOINT}/api/`;

// Create a new mutex
const mutex = new Mutex();

const baseQuery = fetchBaseQuery({
  baseUrl,
});

const customFetchBase: BaseQueryFn<
  string | FetchArgs,
  unknown,
  FetchBaseQueryError
> = async (args, api, extraOptions) => {
  // wait until the mutex is available without locking it
  await mutex.waitForUnlock();
  let result = await baseQuery(args, api, extraOptions);
  if ((result.error?.data as any)?.message === 'You are not logged in') {
    if (!mutex.isLocked()) {
      const release = await mutex.acquire();

      try {
        const refreshResult = await baseQuery(
          { credentials: 'include', url: 'auth/refresh' },
          api,
          extraOptions
        );

        if (refreshResult.data) {
          // Retry the initial query
          result = await baseQuery(args, api, extraOptions);
        } else {
          api.dispatch(logout());
          window.location.href = '/login';
        }
      } finally {
        // release must be called once the mutex should be released again.
        release();
      }
    } else {
      // wait until the mutex is available without locking it
      await mutex.waitForUnlock();
      result = await baseQuery(args, api, extraOptions);
    }
  }

  return result;
};

export default customFetchBase;


Create the API Queries and Mutations with RTK Query

To begin, let’s define the Typescript types required to type the request and response objects.

src/redux/api/type.ts


export interface GenericResponse {
  status: string;
  message: string;
}

export interface IResetPasswordRequest {
  resetToken: string;
  password: string;
  passwordConfirm: string;
}

export interface IPostRequest {
  title: string;
  content: string;
  image: string;
  user: string;
}

export interface IUser {
  name: string;
  email: string;
  role: string;
  photo: string;
  _id: string;
  id: string;
  created_at: string;
  updated_at: string;
  __v: number;
}

export interface IPostResponse {
  id: string;
  title: string;
  content: string;
  image: string;
  category: string;
  user: IUser;
  created_at: string;
  updated_at: string;
}


With RTK Query, it is recommended to put all the API definitions related to a specific resource in a single file.

The API definitions for the CRUD app will reside in the src/redux/api/postApi.ts file.

  • createPost – This method will make a POST request to create a new post in the database.
  • updatePost – This method will make a PATCH request to update the post.
  • getPost – Makes a GET request to retrieve a single post
  • getAllPosts – Makes a GET request to retrieve all the posts
  • deletePost – Makes a DELETE request to delete a post in the database.

src/redux/api/postApi.ts


import { createApi } from '@reduxjs/toolkit/query/react';
import customFetchBase from './customFetchBase';
import { IPostResponse } from './types';

export const postApi = createApi({
  reducerPath: 'postApi',
  baseQuery: customFetchBase,
  tagTypes: ['Posts'],
  endpoints: (builder) => ({
    createPost: builder.mutation<IPostResponse, FormData>({
      query(post) {
        return {
          url: '/posts',
          method: 'POST',
          credentials: 'include',
          body: post,
        };
      },
      invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
      transformResponse: (result: { data: { post: IPostResponse } }) =>
        result.data.post,
    }),
    updatePost: builder.mutation<IPostResponse, { id: string; post: FormData }>(
      {
        query({ id, post }) {
          return {
            url: `/posts/${id}`,
            method: 'PATCH',
            credentials: 'include',
            body: post,
          };
        },
        invalidatesTags: (result, error, { id }) =>
          result
            ? [
                { type: 'Posts', id },
                { type: 'Posts', id: 'LIST' },
              ]
            : [{ type: 'Posts', id: 'LIST' }],
        transformResponse: (response: { data: { post: IPostResponse } }) =>
          response.data.post,
      }
    ),
    getPost: builder.query<IPostResponse, string>({
      query(id) {
        return {
          url: `/posts/${id}`,
          credentials: 'include',
        };
      },
      providesTags: (result, error, id) => [{ type: 'Posts', id }],
    }),
    getAllPosts: builder.query<IPostResponse[], void>({
      query() {
        return {
          url: `/posts`,
          credentials: 'include',
        };
      },
      providesTags: (result) =>
        result
          ? [
              ...result.map(({ id }) => ({
                type: 'Posts' as const,
                id,
              })),
              { type: 'Posts', id: 'LIST' },
            ]
          : [{ type: 'Posts', id: 'LIST' }],
      transformResponse: (results: { data: { posts: IPostResponse[] } }) =>
        results.data.posts,
    }),
    deletePost: builder.mutation<IPostResponse, string>({
      query(id) {
        return {
          url: `/posts/${id}`,
          method: 'Delete',
          credentials: 'include',
        };
      },
      invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
    }),
  }),
});

export const {
  useCreatePostMutation,
  useDeletePostMutation,
  useUpdatePostMutation,
  useGetAllPostsQuery,
} = postApi;


Note: Set credentials: 'include' to tell RTK Query to send the cookies along with the request.

We used the transformResponse property to manipulate the data returned by the server before it hits the cache.

Also, you need to invalidate the cache after every mutation to update the server state.

Connect the Queries and Mutations to the Store

Next, add the postApi reducer to the configureStore reducer object in order to register it in the Redux store.

Also, add the postApi.middleware to the middleware array to enable caching, polling, invalidation, and other essential features of RTK Query.

src/redux/store.ts


import { configureStore } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { authApi } from './api/authApi';
import { postApi } from './api/postApi';
import { userApi } from './api/userApi';
import userReducer from './features/userSlice';
import postReducer from './features/postSlice';

export const store = configureStore({
  reducer: {
    [authApi.reducerPath]: authApi.reducer,
    [userApi.reducerPath]: userApi.reducer,
    // Connect the PostApi reducer to the store
    [postApi.reducerPath]: postApi.reducer,
    userState: userReducer,
    postState: postReducer,
  },
  devTools: process.env.NODE_ENV === 'development',
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({}).concat([
      authApi.middleware,
      userApi.middleware,
      // Add the PostApi middleware to the store
      postApi.middleware,
    ]),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;


Create a Modal Component

src/components/modals/post.modal.tsx


import ReactDom from 'react-dom';
import React, { FC, CSSProperties } from 'react';
import { Container } from '@mui/material';

const OVERLAY_STYLES: CSSProperties = {
  position: 'fixed',
  top: 0,
  left: 0,
  right: 0,
  bottom: 0,
  backgroundColor: 'rgba(0,0,0,.3)',
  zIndex: 1000,
};

const MODAL_STYLES: CSSProperties = {
  position: 'fixed',
  top: '10%',
  left: '50%',
  transform: 'translateX(-50%)',
  transition: 'all 300ms ease',
  backgroundColor: 'white',
  overflowY: 'scroll',
  zIndex: 1000,
};

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 style={OVERLAY_STYLES} onClick={() => setOpenPostModal(false)} />
      <Container
        maxWidth='sm'
        sx={{ p: '2rem 1rem', borderRadius: 1 }}
        style={MODAL_STYLES}
      >
        {children}
      </Container>
    </>,
    document.getElementById('post-modal') as HTMLElement
  );
};

export default PostModal;


Next, add this line of HTML before the closing body tag in the public/index.html file.


<div id="post-modal"></div>

post modal react

React RTK Query DELETE Request

Now create a React component to delete the data with useDeletePostMutation hook auto-generated by RTK Query.

src/components/post/post.component.tsx


import {
  Avatar,
  Box,
  Card,
  CardActions,
  CardContent,
  CardMedia,
  Grid,
  Typography,
} from '@mui/material';
import MoreHorizOutlinedIcon from '@mui/icons-material/MoreHorizOutlined';
import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import { FC, useEffect, useState } from 'react';
import PostModal from '../modals/post.modal';
import { useDeletePostMutation } from '../../redux/api/postApi';
import { toast } from 'react-toastify';
import UpdatePost from './update-post';
import { IPostResponse } from '../../redux/api/types';
import { format, parseISO } from 'date-fns';
import './post.styles.scss';

const SERVER_ENDPOINT = process.env.REACT_APP_SERVER_ENDPOINT;

interface IPostItemProps {
  post: IPostResponse;
}

const PostItem: FC<IPostItemProps> = ({ post }) => {
  const [openPostModal, setOpenPostModal] = useState(false);
  const [deletePost, { isLoading, error, isSuccess, isError }] =
    useDeletePostMutation();

  useEffect(() => {
    if (isSuccess) {
      toast.success('Post deleted successfully');
    }

    if (isError) {
      if (Array.isArray((error as any).data.error)) {
        (error as any).data.error.forEach((el: any) =>
          toast.error(el.message, {
            position: 'top-right',
          })
        );
      } else {
        toast.error((error as any).data.message, {
          position: 'top-right',
        });
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isLoading]);

  const onDeleteHandler = (id: string) => {
    if (window.confirm('Are you sure')) {
      deletePost(id);
    }
  };

  return (
    <>
      <Grid item xs={12} md={6} lg={4}>
        <Card sx={{ maxWidth: 345, overflow: 'visible' }}>
          <CardMedia
            component='img'
            height='250'
            image={`${SERVER_ENDPOINT}/api/static/posts/${post.image}`}
            alt='green iguana'
            sx={{ p: '1rem 1rem 0' }}
          />
          <CardContent>
            <Typography
              gutterBottom
              variant='h5'
              component='div'
              sx={{ color: '#4d4d4d', fontWeight: 'bold', height: '64px' }}
            >
              {post.title.length > 50
                ? post.title.substring(0, 50) + '...'
                : post.title}
            </Typography>
            <Box display='flex' alignItems='center' sx={{ mt: '1rem' }}>
              <Typography
                variant='body1'
                sx={{
                  backgroundColor: '#dad8d8',
                  p: '0.1rem 0.4rem',
                  borderRadius: 1,
                  mr: '1rem',
                }}
              >
                {post.category}
              </Typography>
              <Typography
                variant='body2'
                sx={{
                  color: '#ffa238',
                }}
              >
                {format(parseISO(post.created_at), 'PPP')}
              </Typography>
            </Box>
          </CardContent>
          <CardActions>
            <Box
              display='flex'
              justifyContent='space-between'
              width='100%'
              sx={{ px: '0.5rem' }}
            >
              <Box display='flex' alignItems='center'>
                <Avatar
                  alt='cart image'
                  src={`${SERVER_ENDPOINT}/api/static/users/${post.user.photo}`}
                />
                <Typography
                  variant='body2'
                  sx={{
                    ml: '1rem',
                  }}
                >
                  Codevo
                </Typography>
              </Box>
              <div className='post-settings'>
                <li>
                  <MoreHorizOutlinedIcon />
                </li>
                <ul className='menu'>
                  <li onClick={() => setOpenPostModal(true)}>
                    <ModeEditOutlineOutlinedIcon
                      fontSize='small'
                      sx={{ mr: '0.6rem' }}
                    />
                    Edit
                  </li>
                  <li onClick={() => onDeleteHandler(post.id)}>
                    <DeleteOutlinedIcon
                      fontSize='small'
                      sx={{ mr: '0.6rem' }}
                    />
                    Delete
                  </li>
                </ul>
              </div>
            </Box>
          </CardActions>
        </Card>
      </Grid>
      <PostModal
        openPostModal={openPostModal}
        setOpenPostModal={setOpenPostModal}
      >
        <UpdatePost setOpenPostModal={setOpenPostModal} post={post} />
      </PostModal>
    </>
  );
};

export default PostItem;


Run this command to install the sass package:


yarn add sass
# or
npm install sass

src/components/post/post.styles.scss


.post-settings {
  position: relative;

  li {
    list-style: none;
  }

  &:hover .menu {
    transform: scale(1);
  }

  .menu {
    position: absolute;
    bottom: 10px;
    right: -2px;
    z-index: 99999;
    margin: 0;
    padding: 0.5rem 0;
    border-radius: 4px;
    background-color: white;
    box-shadow: 0 0 6px rgba(0, 0, 0, 0.15);
    transform: scale(0);
    transition: scale 0.2s ease-in-out;

    li {
      display: flex;
      align-items: center;
      justify-self: self-start;
      height: 30px;
      width: 100px;
      padding: 0.7rem 0.5rem;
      cursor: pointer;
      font-size: 16px;
      transition: all 300ms ease-in-out;

      &:hover {
        background-color: #f5f5f5;
      }
    }
  }
}


React RTK Query GET Request

Next, let’s implement the React component to retrieve all the data from the RESTful API using the useGetAllPostsQuery hook.

src/pages/home.page.tsx


import { Box, Container, Grid } from '@mui/material';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import FullScreenLoader from '../components/FullScreenLoader';
import Message from '../components/Message';
import PostItem from '../components/post/post.component';
import { useGetAllPostsQuery } from '../redux/api/postApi';

const HomePage = () => {
  const { isLoading, isError, error, data: posts } = useGetAllPostsQuery();

  useEffect(() => {
    if (isError) {
      if (Array.isArray((error as any).data.error)) {
        (error as any).data.error.forEach((el: any) =>
          toast.error(el.message, {
            position: 'top-right',
          })
        );
      } else {
        toast.error((error as any).data.message, {
          position: 'top-right',
        });
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isLoading]);

  if (isLoading) {
    return <FullScreenLoader />;
  }

  return (
    <Container
      maxWidth={false}
      sx={{ backgroundColor: '#2363eb', height: '100vh' }}
    >
      {posts?.length === 0 ? (
        <Box maxWidth='sm' sx={{ mx: 'auto', py: '5rem' }}>
          <Message type='info' title='Info'>
            No posts at the moment
          </Message>
        </Box>
      ) : (
        <Grid
          container
          rowGap={5}
          maxWidth='lg'
          sx={{
            margin: '0 auto',
            pt: '5rem',
            gridAutoRows: 'max-content',
          }}
        >
          {posts?.map((post) => (
            <PostItem key={post.id} post={post} />
          ))}
        </Grid>
      )}
    </Container>
  );
};

export default HomePage;


React RTK Query POST Request

Next, create a React component to make a POST request to the server with the formData.

src/components/post/create-post.tsx


import {
  Box,
  CircularProgress,
  TextareaAutosize,
  TextField,
  Typography,
} from '@mui/material';
import {
  Controller,
  FormProvider,
  SubmitHandler,
  useForm,
} from 'react-hook-form';
import { object, string, TypeOf, z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import FileUpload from '../FileUpload/FileUpload';
import { LoadingButton } from '@mui/lab';
import { FC, useEffect } from 'react';
import { toast } from 'react-toastify';
import { useCreatePostMutation } from '../../redux/api/postApi';

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

const createPostSchema = object({
  title: string().nonempty('Title is required'),
  content: string().max(50).nonempty('Content is required'),
  category: string().max(50).nonempty('Category is required'),
  image: z.instanceof(File),
});

export type ICreatePost = TypeOf<typeof createPostSchema>;

const CreatePost: FC<ICreatePostProp> = ({ setOpenPostModal }) => {
  const [createPost, { isLoading, isError, error, isSuccess }] =
    useCreatePostMutation();

  const methods = useForm<ICreatePost>({
    resolver: zodResolver(createPostSchema),
  });

  useEffect(() => {
    if (isSuccess) {
      toast.success('Post created successfully');
      setOpenPostModal(false);
    }

    if (isError) {
      if (Array.isArray((error as any).data.error)) {
        (error as any).data.error.forEach((el: any) =>
          toast.error(el.message, {
            position: 'top-right',
          })
        );
      } else {
        toast.error((error as any).data.message, {
          position: 'top-right',
        });
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isLoading]);

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

  const onSubmitHandler: SubmitHandler<ICreatePost> = (values) => {
    const formData = new FormData();

    formData.append('image', values.image);
    formData.append('data', JSON.stringify(values));
    createPost(formData);
  };

  return (
    <Box>
      <Box display='flex' justifyContent='space-between' sx={{ mb: 3 }}>
        <Typography variant='h5' component='h1'>
          Create Post
        </Typography>
        {isLoading && <CircularProgress size='1rem' color='primary' />}
      </Box>
      <FormProvider {...methods}>
        <Box
          component='form'
          noValidate
          autoComplete='off'
          onSubmit={methods.handleSubmit(onSubmitHandler)}
        >
          <TextField
            label='Post Title'
            fullWidth
            sx={{ mb: '1rem' }}
            {...methods.register('title')}
          />
          <TextField
            label='Category'
            fullWidth
            sx={{ mb: '1rem' }}
            {...methods.register('category')}
          />
          <Controller
            name='content'
            control={methods.control}
            defaultValue=''
            render={({ field }) => (
              <TextareaAutosize
                {...field}
                placeholder='Post Details'
                minRows={8}
                style={{
                  width: '100%',
                  border: '1px solid #c8d0d4',
                  fontFamily: 'Roboto, sans-serif',
                  marginBottom: '1rem',
                  outline: 'none',
                  fontSize: '1rem',
                  padding: '1rem',
                }}
              />
            )}
          />
          <FileUpload limit={1} name='image' multiple={false} />
          <LoadingButton
            variant='contained'
            fullWidth
            sx={{ py: '0.8rem', mt: 4, backgroundColor: '#2363eb' }}
            type='submit'
            loading={isLoading}
          >
            Create Post
          </LoadingButton>
        </Box>
      </FormProvider>
    </Box>
  );
};

export default CreatePost;


React RTK Query PATCH Request

Lastly, create a React component with RTK Query to update the post in the database.

src/components/post/update-post.tsx


import {
  Box,
  CircularProgress,
  TextareaAutosize,
  TextField,
  Typography,
} from '@mui/material';
import {
  Controller,
  FormProvider,
  SubmitHandler,
  useForm,
} from 'react-hook-form';
import { object, string, TypeOf, z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import FileUpload from '../FileUpload/FileUpload';
import { LoadingButton } from '@mui/lab';
import { FC, useEffect } from 'react';
import { pickBy } from 'lodash';
import { toast } from 'react-toastify';
import { useUpdatePostMutation } from '../../redux/api/postApi';
import { IPostResponse } from '../../redux/api/types';

interface IUpdatePostProp {
  setOpenPostModal: (openPostModal: boolean) => void;
  post: IPostResponse;
}

const updatePostSchema = object({
  title: string(),
  content: string().max(50),
  category: string().max(50),
  image: z.instanceof(File),
}).partial();

type IUpdatePost = TypeOf<typeof updatePostSchema>;

const UpdatePost: FC<IUpdatePostProp> = ({ setOpenPostModal, post }) => {
  const [updatePost, { isLoading, isError, error, isSuccess }] =
    useUpdatePostMutation();

  const methods = useForm<IUpdatePost>({
    resolver: zodResolver(updatePostSchema),
  });

  useEffect(() => {
    if (isSuccess) {
      toast.success('Post updated successfully');
      setOpenPostModal(false);
    }

    if (isError) {
      if (Array.isArray((error as any).data.error)) {
        (error as any).data.error.forEach((el: any) =>
          toast.error(el.message, {
            position: 'top-right',
          })
        );
      } else {
        toast.error((error as any).data.message, {
          position: 'top-right',
        });
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isLoading]);

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

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

  const onSubmitHandler: SubmitHandler<IUpdatePost> = (values) => {
    const formData = new FormData();
    const filteredFormData = pickBy(
      values,
      (value) => value !== '' && value !== undefined
    );
    const { image, ...otherFormData } = filteredFormData;
    if (image) {
      formData.append('image', image);
    }
    formData.append('data', JSON.stringify(otherFormData));
    updatePost({ id: post?.id!, post: formData });
  };

  return (
    <Box>
      <Box display='flex' justifyContent='space-between' sx={{ mb: 3 }}>
        <Typography variant='h5' component='h1'>
          Edit Post
        </Typography>
        {isLoading && <CircularProgress size='1rem' color='primary' />}
      </Box>
      <FormProvider {...methods}>
        <Box
          component='form'
          noValidate
          autoComplete='off'
          onSubmit={methods.handleSubmit(onSubmitHandler)}
        >
          <TextField
            label='Title'
            fullWidth
            sx={{ mb: '1rem' }}
            {...methods.register('title')}
          />
          <TextField
            label='Category'
            fullWidth
            sx={{ mb: '1rem' }}
            {...methods.register('category')}
          />
          <Controller
            name='content'
            control={methods.control}
            defaultValue=''
            render={({ field }) => (
              <TextareaAutosize
                {...field}
                placeholder='Post Details'
                minRows={8}
                style={{
                  width: '100%',
                  border: '1px solid #c8d0d4',
                  fontFamily: 'Roboto, sans-serif',
                  marginBottom: '1rem',
                  outline: 'none',
                  fontSize: '1rem',
                  padding: '1rem',
                }}
              />
            )}
          />
          <FileUpload limit={1} name='image' multiple={false} />
          <LoadingButton
            variant='contained'
            fullWidth
            sx={{ py: '0.8rem', mt: 4, backgroundColor: '#2363eb' }}
            type='submit'
            loading={isLoading}
          >
            Edit Post
          </LoadingButton>
        </Box>
      </FormProvider>
    </Box>
  );
};

export default UpdatePost;


Conclusion

With this React, RTK Query, and Redux Toolkit example, you’ve learned the different ways to perform CRUD operations against a RESTful API.

Check out the source code on GitHub