React Query is considered a server state management library for ReactJs. This tutorial will teach you how to build a React Query and Axios CRUD RESTful API application to perform the Create/Update/Get/Delete operations.

More practice:

Build React Query, and Axios CRUD application

Benefits of using React Query

React query is often described as the missing server state management library for React created by Tanner Linsley in 2019. To be more precise, it makes fetching, caching, synchronizing, and updating server state in your React applications a breeze.

There are other server state management libraries like SWR, Apollo Client, and RTK Query and you can see their benchmark with React Query on their official website.

According to the benchmark, React Query outperforms its competitors in most aspects, making it the best option for any React.js project.

Below are some benefits of using React Query:

  • Invalidating “out of date” data to request a fresh one
  • Deduping multiple requests for the same data into a single request
  • Managing server state memory and garbage collection
  • Cache server state to make the application feel much faster and more responsive.
  • Makes your application more scalable and maintainable.

React Query and Axios Overview

We will build a React.js, MUI v5, and Typescript client with React Query and Axios to make CRUD operations against a RESTful API:

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

-On the homepage, a GET request is made to the server to retrieve all the posts.

react query CRUD RESTful application with axios homepage overview

-To create a new post in the database, click on “CREATE POST” from the navigation menu to display the create modal.

Next, provide the required information and make a POST request to the server by clicking the “CREATE POST” button.

React Query CRUD Restful app with axios make post request

-To edit a post, hover over the three dots on the post and click on the edit button to open the edit popup.

React Query CRUD Restful app with axios click to edit post

When the edit modal opens, the fields will be automatically filled. After making the changes, make a PATCH request to update the post on the server by clicking the “EDIT POST” button.

React Query CRUD Restful app with axios make patch request

-To delete a post, hover over the three dots on the post and click the delete button. You will be prompted to confirm your action before a DELETE request will be made to the server to remove that post from the database.

React Query CRUD Restful app with axios make delete request

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

Setup React Query with Axios and Other Dependencies

Run this command in your terminal to create a React.js boilerplate application in a react-query-axios directory.


yarn create react-app react-query-axios --template typescript
# or
npx create-react-app react-query-axios --template typescript

Install Material UI, React-Hook-Form, and React-Router-Dom

Run the following commands to install React Query, Axios, Material UI v5, and React-Hook-Form:

Material UI v5


yarn add @mui/material @emotion/react @emotion/styled @mui/lab
# or 
npm install @mui/material @emotion/react @emotion/styled @mui/lab

React-Hook-Form and Zod


yarn add react-hook-form @hookform/resolvers zod
# or 
npm install react-hook-form @hookform/resolvers zod

Other dependencies


yarn add react-toastify react-router-dom react-cookie date-fns lodash && yarn add -D @types/lodash
# or 
npm install react-toastify react-router-dom react-cookie date-fns lodash && yarn add -D @types/lodash

Install React Query and Axios

React Query


yarn add @tanstack/react-query @tanstack/react-query-devtools
# or
npm install @tanstack/react-query @tanstack/react-query-devtools

Note: React Query is compatible with React v16.8+ and works well with ReactDOM and React Native.

To configure React Query with React.js, we need to wrap the QueryClientProvider component around the entry point of our application and provide it with the QueryClient instance.


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient();

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <react.strictmode>
    <queryclientprovider client="{queryClient}">
      <app />
      <reactquerydevtools initialisopen="{false}" />
   </queryclientprovider>
  </react.strictmode>
);

Out of the box“, React Query works amazing with zero-config and can be customized to meet your requirements as your application grows.

By default, React Query considers cached queries as stale (the default staleTime: 0) and the stale queries are prefetched in the background when:

  • the window is refocused
  • the network reconnects
  • a query is configured with a re-fetch interval
  • new instances of the query mounts

We can change the default behaviors by either configuring the queries globally or per query.

To change the default behaviors globally, you need to pass a defaultOptions config object to the QueryClient() .


const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
      refetchOnmount: false,
      refetchOnReconnect: false,
      retry: 1,
      staleTime: 5 * 1000,
    },
  },
});

  • refetchOnWindowFocus : Defaults to true . The query will not re-fetch on window focus if set to false .
  • refetchOnmount : Defaults to true . The query will not re-fetch on mount if set to false.
  • refetchOnReconnect : Defaults to true . The query will not re-fetch on reconnect if set to false .
  • retry : If set to false , failed queries will not retry by default. On the other hand, if set to true, failed queries will retry infinitely.
  • staleTime : Defaults to 0 . The time in milliseconds after the data becomes stale.

Axios


yarn add axios
# or 
npm install axios

Creating the Typescript Interfaces

Now that we have installed all the necessary dependencies, it’s now time to create the data types with TypeScript.

I assume you’ve already implemented the authentication part in React Query, Context API, Axios Interceptors JWT Authentication.

You need to be authenticated by the server before you can perform the CRUD operations.

Open the ./src/api/types.ts file and add the following TypeScript interfaces.

src/api/types.ts


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

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

export interface IPostsResponse {
  status: string;
  data: {
    posts: IPostResponse[];
  };
}

Creating the API Services with Axios

Create a src/api/postApi.ts file and add the following code to create the Create/Patch/Get/Delete services.

src/api/postApi.ts


import { authApi } from './authApi';
import { GenericResponse, IPostResponse, IPostsResponse } from './types';

export const getAllPostsFn = async () => {
  const response = await authApi.get<IPostsResponse>(`posts`);
  return response.data;
};

export const getPostFn = async (id: string) => {
  const response = await authApi.get<IPostResponse>(`posts/${id}`);
  return response.data;
};

export const createPostFn = async (formData: FormData) => {
  const response = await authApi.post<IPostResponse>(`posts`, formData, {
    headers: {
      'Content-Type': 'multipart/form-data',
    },
  });
  return response.data;
};

export const updatePostFn = async ({
  id,
  formData,
}: {
  id: string;
  formData: FormData;
}) => {
  const response = await authApi.patch<IPostResponse>(`posts/${id}`, formData, {
    headers: {
      'Content-Type': 'multipart/form-data',
    },
  });
  return response.data;
};

export const deletePostFn = async (id: string) => {
  const response = await authApi.delete<GenericResponse>(`posts/${id}`);
  return response.data;
};
  • getAllPostsFn – Makes Axios GET request to retrieve all posts
  • getPostFn – Makes Axios GET request to retrieve a single post
  • createPostFn – Makes Axios POST request to create a new post
  • updatePostFn – Makes Axios PATCH request to update a post
  • deletePostFn – Makes Axios DELETE request to remove a post from the database.

Note: Set 'Content-Type': 'multipart/form-data' for the CREATE and UPDATE CRUD operations to enable Axios to send the form data to the server.

Creating the 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 within the body tags in the public/index.html file.


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

post modal react

React Query and Axios DELETE Request

To modify data on the server (CREATE, PATCH, PUT, or DELETE operations), we use the React Query useMutation hook.


const {
    mutate: deletePost,
    isLoading,
    isError,
    error,
    data,
    isSuccess,
  } = useMutation((id: string) => deletePostFn(id), {
    onSuccess(data) {},
    onError(error: any) {},
  });
  • deletePostFn (mutationFn): a function that returns a promise after performing an asynchronous task.
  • onSuccess : a function that will be evoked when the mutation is successful and gets the data returned as a parameter.
  • onError : a function that will be evoked when the mutation results in an error.

The object returned by the useMutation() hook contains the following:

  • mutate : a function to manually trigger the mutation
  • isLoading : a boolean showing whether the mutation is currently executing.
  • isError : a boolean showing whether the mutation resulted in an error.
  • error : the error returned when the mutation fails.
  • data : the data returned when the mutation is successful.
  • isSuccess : a boolean indicating whether the mutation was successful.

You can visit the official React Query website to learn more about the useMutation() hook.

Note: For any of the mutations, you need to invalidate the query cache so that React Query can re-fetch the most current data from the database.


import { useMutation, useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
queryClient.invalidateQueries(['posts']);

With that out of the way, let’s use React Query and Axios to delete a post from the database.

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, useState } from 'react';
import PostModal from '../modals/post.modal';
import { toast } from 'react-toastify';
import UpdatePost from './update-post';
import { format, parseISO } from 'date-fns';
import './post.styles.scss';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { deletePostFn } from '../../api/postApi';
import { IPostResponse } from '../../api/types';

const SERVER_ENDPOINT = process.env.REACT_APP_SERVER_ENDPOINT;

interface IPostItemProps {
  post: IPostResponse;
}

const PostItem: FC<IPostItemProps> = ({ post }) => {
  const queryClient = useQueryClient();
  const [openPostModal, setOpenPostModal] = useState(false);

  const { mutate: deletePost } = useMutation((id: string) => deletePostFn(id), {
    onSuccess(data) {
      queryClient.invalidateQueries('posts');
      toast.success('Post deleted successfully');
    },
    onError(error: any) {
      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',
        });
      }
    },
  });

  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' }}
            >
              {post.title.length > 20
                ? `${post.title.substring(0, 20)}...`
                : 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 Sass


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 Query and Axios GET Request

To retrieve data from the server, we use the React Query useQuery() hook:


  const {
    isSuccess,
    isLoading,
    refetch,
    isError,
    error,
    data
   } = useQuery(['posts'], () => getAllPostsFn(), {
    enabled: false,
    select: (data) => {},
    retry: 1,
    onSuccess: (data) => {},
    onError: (error) => {},
  });
  • getAllPostsFn (queryFn): a function used by the useQuery() hook to request data.
  • posts (queryKey): a unique key for managing the query cache.
  • enabled : Defaults to true . The query will be disabled from running automatically when set to false .
  • retry : If set to true , failed queries will retry infinitely.
  • onSuccess : This callback function will be fired anytime the query successfully fetches new data.
  • onError : This function will be evoked if the query encounters an error.
  • select : This function can be used to transform the data returned by the query.
  • refetch : A function to manually re-fetch the query.

Visit the official React Query documentation to read more about the useQuery() hook.

Before writing the code to fetch all the posts from the database, let’s create a message component that will be displayed when there are no posts in the database.

src/components/Message.tsx


import { Alert, AlertColor, AlertProps, AlertTitle } from '@mui/material';
import React from 'react';

type IMessageProps = {
  children: React.ReactNode;
  type: AlertColor;
  title: React.ReactNode;
} & AlertProps;

const Message: React.FC<IMessageProps> = ({
  children,
  title,
  type,
  ...otherAlertProps
}) => {
  return (
    <Alert {...otherAlertProps} severity={type}>
      <AlertTitle>{title}</AlertTitle>
      {children}
    </Alert>
  );
};

export default Message;

Now let’s create the React Typescript component with Material UI to get all the posts from the database using React Query and Axios.

src/pages/home.page.tsx


import { Box, Container, Grid } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import { getAllPostsFn } from '../api/postApi';
import FullScreenLoader from '../components/FullScreenLoader';
import PostItem from '../components/post/post.component';
import Message from '../components/Message';

const HomePage = () => {
  const { isLoading, data: posts } = useQuery(['posts'], () => getAllPostsFn(), {
    select: (data) => data.data.posts,
    onError: (error) => {
      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',
        });
      }
    },
  });

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

  return (
    <Container
      maxWidth={false}
      sx={{ backgroundColor: '#2363eb', minHeight: '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' }}
        >
          {posts?.map((post) => (
            <PostItem key={post.id} post={post} />
          ))}
        </Grid>
      )}
    </Container>
  );
};

export default HomePage;

React Query and Axios POST Request

Since we are also uploading the post image to the server, let’s create a simple Material UI and React-Hook-Form file uploader component.

src/components/FileUpLoader.tsx


import { FormHelperText } from '@mui/material';
import React, { useCallback } from 'react';
import { Controller, useController, useFormContext } from 'react-hook-form';

type FileUpLoaderProps = {
  multiple?: boolean;
  name: string;
};
const FileUpLoader: React.FC<FileUpLoaderProps> = ({
  name,
  multiple = false,
}) => {
  const {
    control,
    formState: { errors },
  } = useFormContext();
  const { field } = useController({ name, control });

  const onFileDrop = useCallback(
    (e: React.SyntheticEvent<EventTarget>) => {
      const target = e.target as HTMLInputElement;
      if (!target.files) return;
      const newFile = Object.values(target.files).map((file: File) => file);
      field.onChange(newFile[0]);
    },

    [field]
  );
  return (
    <Controller
      name={name}
      defaultValue=''
      control={control}
      render={({ field: { name, onBlur, ref } }) => (
        <>
          <input
            type='file'
            name={name}
            onBlur={onBlur}
            ref={ref}
            onChange={onFileDrop}
            multiple={multiple}
            accept='image/jpg, image/png, image/jpeg'
          />
          <FormHelperText error={!!errors[name]}>
            {errors[name] ? errors[name]?.message : ''}
          </FormHelperText>
        </>
      )}
    />
  );
};

export default FileUpLoader;

Now, let’s create a React Typescript component to add a new post to the database using React Query and Axios POST request.

src/components/post/create-post.tsx


import {
  Box,
  CircularProgress,
  FormHelperText,
  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 { LoadingButton } from '@mui/lab';
import { FC, useEffect } from 'react';
import { toast } from 'react-toastify';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createPostFn } from '../../api/postApi';
import FileUpLoader from '../FileUpLoader';

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

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

export type ICreatePost = TypeOf<typeof createPostSchema>;

const CreatePost: FC<ICreatePostProp> = ({ setOpenPostModal }) => {
  const queryClient = useQueryClient();
  const { isLoading, mutate: createPost } = useMutation(
    (post: FormData) => createPostFn(post),
    {
      onSuccess: () => {
        queryClient.invalidateQueries(['posts']);
        toast.success('Post created successfully');
        setOpenPostModal(false);
      },
      onError: (error: any) => {
        setOpenPostModal(false);
        if (Array.isArray(error.response.data.error)) {
          error.data.error.forEach((el: any) =>
            toast.error(el.message, {
              position: 'top-right',
            })
          );
        } else {
          toast.error(error.response.data.message, {
            position: 'top-right',
          });
        }
      },
    }
  );

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

  const {
    formState: { errors, isSubmitSuccessful },
  } = methods;

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

  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')}
          />
          <FormHelperText error={!!errors['title']}>
            {errors['title'] ? errors['title'].message : ''}
          </FormHelperText>
          <TextField
            label='Category'
            fullWidth
            sx={{ mb: '1rem' }}
            {...methods.register('category')}
          />
          <FormHelperText error={!!errors['category']}>
            {errors['category'] ? errors['category'].message : ''}
          </FormHelperText>

          <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',
                    outline: 'none',
                    fontSize: '1rem',
                    padding: '1rem',
                  }}
                />
                <FormHelperText error={!!errors[field.name]}>
                  {errors[field.name] ? errors[field.name]?.message : ''}
                </FormHelperText>
              </>
            )}
          />
          <FileUpLoader name='image' />
          <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 Query and Axios PATCH Request

Finally, let’s create a React Material v5 component to update a post in the database using React Query and Axios.


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 { LoadingButton } from '@mui/lab';
import { FC, useEffect } from 'react';
import { pickBy } from 'lodash';
import { toast } from 'react-toastify';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updatePostFn } from '../../api/postApi';
import { IPostResponse } from '../../api/types';
import FileUpLoader from '../FileUpLoader';

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 queryClient = useQueryClient();
  const { isLoading, mutate: updatePost } = useMutation(
    ({ id, formData }: { id: string; formData: FormData }) =>
      updatePostFn({ id, formData }),
    {
      onSuccess: () => {
        queryClient.invalidateQueries(['posts']);
        toast.success('Post updated successfully');
        setOpenPostModal(false);
      },
      onError: (error: any) => {
        setOpenPostModal(false);
        if (Array.isArray(error.response.data.error)) {
          error.data.error.forEach((el: any) =>
            toast.error(el.message, {
              position: 'top-right',
            })
          );
        } else {
          toast.error(error.response.data.message, {
            position: 'top-right',
          });
        }
      },
    }
  );

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

  const {
    formState: { isSubmitting },
  } = methods;

  useEffect(() => {
    if (isSubmitting) {
      methods.reset();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [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!, 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',
                  outline: 'none',
                  fontSize: '1rem',
                  padding: '1rem',
                }}
              />
            )}
          />
          <FileUpLoader name='image' />
          <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 Query, MUI v5, React-Hook-Form, and Axios example in Typescript, you’ve learned how to perform the basic CRUD (CREATE, READ, UPDATE, and DELETE) operations against a RESTful API.

React Query and Axios CRUD App Source Code

Check out the complete source code for: