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:
- React CRUD example with Redux Toolkit, RTK Query & REST API
- Vue.js, Pinia, Vue Query, Axios Interceptors JWT Authentication
- React, Material UI and React Hook Form: Login and Signup Forms
- React, RTK Query, React Hook Form and Material UI – Image Upload
- React + Redux Toolkit: JWT Authentication and Authorization
- React.js + Redux Toolkit: Refresh Tokens Authentication
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 METHOD | ROUTE | DESCRIPTION |
---|---|---|
GET | /api/posts | Retrieve all posts |
POST | /api/posts | Create new post |
GET | /api/posts/:id | Get a single post |
PATCH | /api/posts/:id | Updates a post |
DELETE | /api/posts/:id | Deletes a post |
-On the homepage, a GET request is made to the server to retrieve all the posts.
-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.
-To edit a post, hover over the three dots on the post and click on the edit button to open the edit popup.
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.
-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.
You can follow one of these tutorials to build the CRUD RESTful API:
- Node.js, Express, TypeORM, PostgreSQL: CRUD Rest API
- Build CRUD RESTful API Server with Golang, Gin, and MongoDB
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 totrue
. The query will not re-fetch on window focus if set tofalse
.refetchOnmount
: Defaults totrue
. The query will not re-fetch on mount if set tofalse
.refetchOnReconnect
: Defaults totrue
. The query will not re-fetch on reconnect if set tofalse
.retry
: If set tofalse
, failed queries will not retry by default. On the other hand, if set totrue
, failed queries will retry infinitely.staleTime
: Defaults to0
. 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 postsgetPostFn
– Makes Axios GET request to retrieve a single postcreatePostFn
– Makes Axios POST request to create a new postupdatePostFn
– Makes Axios PATCH request to update a postdeletePostFn
– 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>
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 mutationisLoading
: 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 theuseQuery()
hook to request data.posts
(queryKey): a unique key for managing the query cache.enabled
: Defaults totrue
. The query will be disabled from running automatically when set tofalse
.retry
: If set totrue
, 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: