By Hemanta Sundaray on 2022-09-17
In part-3, we learnt how to create new blog posts on the server using the useMutation hook. In this post, we will learn how to edit/update them.
Updating a blog post is similar to creating a blog post. Instead of sending a POST request, we send a PUT request.
First, let’s create an Edit Post button and display it next to each blog post on the Posts page. Edit the Posts.js file as shown below:
import React from "react";
import { Link } from "react-router-dom";
import { useGetPosts } from "./postHooks";
const Posts = () => {
const { isLoading, isError, error, data: posts } = useGetPosts();
return (
<div className="w-1/2 m-auto mt-6">
<Link to="/posts/create">
<button className="bg-blue-500 text-gray-50 px-2 py-1 rounded hover:bg-blue-600">
Create Post
</button>
</Link>
<h1 className="text-2xl font-bold mb-2">Blog Posts</h1>
{isLoading ? (
<p>Loading...</p>
) : isError ? (
<p>{error.message}</p>
) : (
posts.map(({ id, title }) => (
<div className="flex justify-even">
<Link key={id} to={`/posts/${id}`}>
<h1 className="mr-6">{title}</h1>
</Link>
<Link to={`/posts/edit/${id}`}>
<button className="bg-green-500 text-gray-50 mb-4 px-1 py-0.5 rounded hover:bg-green-600">
Edit Post
</button>
</Link>
</div>
))
)}
</div>
);
};
export default Posts;
Clicking on the Edit Post button will take us to the route, which will load the /posts/edit/${id}
Editpost component.
In the components folder, create a file named Editpost.js with the following code snippets.
import React, { useState } from "react";
import { useParams } from "react-router-dom";
import { Formik, Field, Form, ErrorMessage } from "formik";
import * as Yup from "yup";
import { useEditPost } from "./postHooks";
import { useGetSinglePostById } from "./postHooks";
const EditPost = () => {
const { id } = useParams();
const { data: post } = useGetSinglePostById(id);
const [error, setError] = useState(null);
const { mutate, isLoading } = useEditPost(id, setError);
return (
<>
{error && (
<div className="w-1/3 flex justify-center items-center bg-red-400 rounded mt-4 px-2 py-2 m-auto">
<p>{error}</p>
</div>
)}
<Formik
initialValues={{ title: post?.title, body: post?.body }}
validationSchema={Yup.object({
title: Yup.string().required("Required"),
body: Yup.string().required("Required"),
})}
onSubmit={(values) => {
mutate(values);
}}
>
<Form className="w-1/3 flex flex-col bg-white shadow-md rounded mt-6 px-8 py-8 m-auto">
<div className="flex flex-col mb-4">
<label
htmlFor="title"
className="text-gray-700 text-sm font-bold mb-2"
>
Title
</label>
<Field
name="title"
type="text"
className="shadow appearance-none border rounded py-1 px-2 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="username"
/>
<ErrorMessage name="title" />
</div>
<div className="flex flex-col mb-4">
<label
htmlFor="body"
className="text-gray-700 text-sm font-bold mb-2"
>
Body
</label>
<Field
name="body"
as="textarea"
type="text"
className="shadow appearance-none border rounded mb-1 py-1 px-2 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage name="body" />
</div>
<button
type="submit"
className="bg-blue-600 text-gray-50 px-2 py-1 rounded hover:bg-blue-700"
>
{isLoading ? "Editing... Post" : "Edit Post"}
</button>
</Form>
</Formik>
</>
);
};
export default EditPost;
On line 11, we are fetching the blog post which we want to edit. And on line 25, we are using the title and body of the post that we fetched to populate the form. On line 25, notice that we have used the optional chaining operator(?.) to ensure that the post object is not null or undefined before accessing its title & body properties.
Next, add the following highlighted code snippets in the postHooks.js file.
import { useNavigate } from "react-router-dom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
export const useGetPosts = () => {
return useQuery(["posts"], async () => {
const { data } = await axios.get(`http://localhost:5000/posts`);
return data;
});
};
export const useGetSinglePostById = (id) => {
return useQuery(["posts", id], async () => {
const { data } = await axios.get(`http://localhost:5000/posts/${id}`);
return data;
});
};
export const useCreatePost = (setError) => {
const queryClient = useQueryClient();
const navigate = useNavigate();
return useMutation(
(postData) => {
return axios.post("http://localhost:5000/posts", postData);
},
{
onSuccess: () => {
queryClient.invalidateQueries(["posts"]);
navigate("/");
},
onError: ({ message }) => {
setError(message);
},
}
);
};
export const useEditPost = (id, setError) => {
const queryClient = useQueryClient();
const navigate = useNavigate();
return useMutation(
(postData) => {
return axios.put(`http://localhost:5000/posts/${id}`, postData);
},
{
onSuccess: () => {
queryClient.invalidateQueries(["posts"]);
navigate("/");
},
onError: ({ message }) => {
setError(message);
},
}
);
};
Finally, add the following highlighted code snippets in the App.js file.
import React from "react";
import { Routes, Route } from "react-router-dom";
import Posts from "./components/Posts";
import SinglePost from "./components/SinglePost";
import CreatePost from "./components/CreatePost";
import EditPost from "./components/Editpost";
const App = () => {
return (
<Routes>
<Route path="/" element={<Posts />} />
<Route path="/posts/:id" element={<SinglePost />} />
<Route path="/posts/create" element={<CreatePost />} />
<Route path="/posts/edit/:id" element={<EditPost />} />
</Routes>
);
};
export default App;
Now, let’s check our edit post functionality.
Go ahead, click on any one of the Edit Post buttons on the Posts page.
Oops! The edit post form is empty. Now, go back to the Posts page and click on the Edit Post button (that you clicked before) again.
We see the edit post form being populated with the post data fetched from the server. But the problem is the post edit form is not getting populated on the first render of the <Editpost /> component.
Why so?
The reason the post data form is empty on the first render is because our data is undefined on the first render cycle. We must fetch the data first.
So, how do we solve our problem?
One of the ways is to split <Editpost /> into two components.
Inside the components folder, create a file named EditPostForm.js with the following code snippets.
import React, { useState } from "react";
import { useParams } from "react-router-dom";
import { Formik, Field, Form, ErrorMessage } from "formik";
import * as Yup from "yup";
import { useEditPost } from "./postHooks";
const EditPostForm = ({ formData: { title, body } }) => {
const { id } = useParams();
const [error, setError] = useState(null);
const { mutate, isLoading } = useEditPost(id, setError);
return (
<>
{error && (
<div className="w-1/3 flex justify-center items-center bg-red-400 rounded mt-4 px-2 py-2 m-auto">
<p>{error}</p>
</div>
)}
<Formik
initialValues={{ title, body }}
validationSchema={Yup.object({
title: Yup.string().required("Required"),
body: Yup.string().required("Required"),
})}
onSubmit={(values) => {
mutate(values);
}}
>
<Form className="w-1/3 flex flex-col bg-white shadow-md rounded mt-6 px-8 py-8 m-auto">
<div className="flex flex-col mb-4">
<label
htmlFor="title"
className="text-gray-700 text-sm font-bold mb-2"
>
Title
</label>
<Field
name="title"
type="text"
className="shadow appearance-none border rounded py-1 px-2 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="username"
/>
<ErrorMessage name="title" />
</div>
<div className="flex flex-col mb-4">
<label
htmlFor="body"
className="text-gray-700 text-sm font-bold mb-2"
>
Body
</label>
<Field
name="body"
as="textarea"
type="text"
className="shadow appearance-none border rounded mb-1 py-1 px-2 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
/>
<ErrorMessage name="body" />
</div>
<button
type="submit"
className="bg-blue-600 text-gray-50 px-2 py-1 rounded hover:bg-blue-700"
>
{isLoading ? "Editing... Post" : "Edit Post"}
</button>
</Form>
</Formik>
</>
);
};
export default EditPostForm;
Now, copy and paste the following code snippets in the EditPost.js file.
import React from "react";
import { useParams } from "react-router-dom";
import { useGetSinglePostById } from "./postHooks";
import EditPostForm from "./EditPostForm";
const EditPost = () => {
const { id } = useParams();
const { data, isLoading } = useGetSinglePostById(id);
return (
<>
{isLoading ? (
<p className="text-center">Loading...</p>
) : (
<EditPostForm formData={data} />
)}
</>
);
};
export default EditPost;
In the <EditPost /> component, we fetch the post data and pass that data to the <EditPostForm /> component through the formData prop. This fixes our problem.
Let’s edit a blog post. Click on the Edit Post button next to the What is React Query? blog post. Make the following changes and click on the Edit Post button.
We get redirected to the Posts page and we see the updated post.
Now you know how to update resources on the server using the useMutation hook.
In part-5, we will learn how to delete resources on the server.