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:
- Form Validation with React Hook Form, Material UI, React and TypeScript
- How I Setup Redux Toolkit and RTK Query the right way
- 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
Related Articles:
Run the Frontend and Backend Apps
To successfully run both the backend and frontend projects on your machine, follow these step-by-step instructions:
- 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.
- 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. - Navigate to the backend directory using the command
cd ./backend
. Runyarn install
to install all required dependencies. - Open the
backend/src/app.ts
file and uncomment the Nodemailer code.
Then, run the commandyarn 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 thebackend/.env
file. Afterward, comment out the Nodemailer code in thebackend/src/app.ts
file, and save the file to restart the Node.js development server. - In another terminal, move to the frontend directory from the root level using the command
cd ./frontend
. Runyarn install
to install all necessary dependencies. Once the installation is complete, executeyarn dev
to start the Vite development server. - 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.
Note: Do not visit the app usinghttp://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.
-To retrieve all the posts from the database, make a GET request to the server.
Now to edit the post, hover over the three dots and click on the edit button to open the edit modal.
Next, issue a PATCH request with the form data to update the post in the database.
-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.
The React RTK Query works with the following API endpoints:
HTTP METHOD | ROUTE | DESCRIPTION |
---|---|---|
GET | /api/posts | Retrieve all posts |
POST | /api/posts | Creates a new post |
GET | /api/posts/:id | Returns a single post |
PATCH | /api/posts/:id | Updates a post |
DELETE | /api/posts/:id | Deletes a post |
You can follow one of these tutorials to build the RESTful API:
- Node.js, Express, TypeORM, PostgreSQL: CRUD Rest API
- Build CRUD RESTful API Server with Golang, Gin, and MongoDB
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:
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 postgetAllPosts
– Makes a GET request to retrieve all the postsdeletePost
– 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>
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