javascriptreactjsreact-querytanstackreact-query

react-query useMutation does not update new changes on the UI


I have a React 18 app. I am using @tanstack/react-query, version 5 for state management.

Firstly, when I click the like button, my UI does not change. Typical behavior should be that the number of likes should increase by one.

clicking like button does not update ui

Secondly, when I reload my page I get the below errors in my browser console and my browser view is blank.

error on reload

Not sure, what I am doing wrong here.

Below is my code:

App.jsx file:

import { useState } from "react";
import MenuBar from "./components/MenuBar";
import Notification from "./components/Notification";
import BlogsPage from "./pages/BlogsPage";
import LoginPage from "./pages/LoginPage";
import UsersPage from "./pages/UsersPage";
import { Route, Routes, useMatch } from "react-router-dom";
import User from "./components/User";
import { useQueryClient } from "@tanstack/react-query";
import Blog from "./components/Blog";

const App = () => {
  const queryClient = useQueryClient();
  const allBlogs = queryClient.getQueryData(['blogs']);
  const allUsers = queryClient.getQueryData(['users']);

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");

  const userMatch = useMatch("/users/:id");
  console.log('userMatch', userMatch);
  const singleUserInfo = userMatch
    ? allUsers.find(user => user.id === userMatch.params.id)
    : null;
  console.log('singleUserInfo', singleUserInfo);

  const blogMatch = useMatch("/blogs/:id");
  console.log('blogMatch', blogMatch);
  const singleBlogInfo = blogMatch
    ? allBlogs.find(blog => blog.id === blogMatch.params.id)
    : null;
  console.log('singleBlogInfo', singleBlogInfo);

  return (
    <div>
      <h1>Blogs App</h1>

      <MenuBar setUsername={setUsername} setPassword={setPassword} />

      <Notification />

      <LoginPage 
        username={username} 
        password={password} 
        setUsername={setUsername} 
        setPassword={setPassword} 
      />

      <Routes>
        <Route path="/users/:id" element={<User user={singleUserInfo} />} />
        <Route path="/blogs/:id" element={<Blog blog={singleBlogInfo} />} />
        <Route path="/users" element={<UsersPage />} />
        <Route path="/" element={<BlogsPage />} />
      </Routes>
    </div>
  );
};

export default App;

Blog.jsx file:

import PropTypes from "prop-types";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNotificationDispatch } from "../context/NotificationContext";
import blogService from "../services/blogs";
import { removeNotificationAfterFiveSeconds, setNotification } from "../helper/notificationSetOrRemove";
import { useUserValue } from "../context/UserContext";
import { Link } from "react-router-dom";

const Blog = ({ blog }) => {
  console.log('Blog.jsx', blog);

  const queryClient = useQueryClient();
  const notificationDispatch = useNotificationDispatch();
  const userLoggedIn = useUserValue();
  
  const updateBlogMutation = useMutation({
    mutationFn: blogService.updateBlog,
    // mutationFn: (blog) => blogService.updateBlog(blog),
    onSuccess: (updatedBlog) => {
      // queryClient.invalidateQueries('blogs');
      queryClient.invalidateQueries({ queryKey: ['blogs'] }, updatedBlog);
    },
    onError: (error, blogData) => {
      console.error(`UPDATE MUTATION ERROR: ${error.message}`);
      setNotification(notificationDispatch, `UPDATE MUTATION ERROR: Blog '${blogData.title}' was already removed from server`, true);
      removeNotificationAfterFiveSeconds(notificationDispatch);
    }
  });

  const handleIncreaseLikes = () => {
    updateBlogMutation.mutate({ ...blog, likes: blog.likes + 1 });
  };

  return (
    <div>
      <h1 data-testid="title">{blog.title}</h1>
      <h4 data-testid="author">Author: {blog.author}</h4>

      <div data-testid="url">
        <Link to={blog.url} target="_blank">{blog.url}</Link>
      </div>

      <div>
        <div>
          <div>
            <span data-testid="likes">Likes: {blog.likes}</span>
            <button onClick={handleIncreaseLikes} className="likeBtn">
              like
            </button>
          </div>

          <div data-testid="addedBy">Added by: {blog.user.name}</div>
        </div>
      </div>
    </div>
  );
};

Blog.propTypes = {
  blog: PropTypes.shape({
    id: PropTypes.string,
    title: PropTypes.string.isRequired,
    author: PropTypes.string.isRequired,
    url: PropTypes.string.isRequired,
    likes: PropTypes.number,
    user: PropTypes.shape({
      id: PropTypes.string.isRequired,
      name: PropTypes.string.isRequired,
      username: PropTypes.string.isRequired,
    }),
  }),
};

export default Blog;

/src/services/blogs.js file:

import axios from 'axios';
const baseUrl = '/api/blogs';

const updateBlog = async (updatedBlog) => {
  const response = await axios.put(`${baseUrl}/${updatedBlog.id}`, updatedBlog);
  return response.data;
};

export default { updateBlog };

BlogList.jsx file:

import { useQuery } from "@tanstack/react-query";
import blogService from "../services/blogs";
import { Link } from "react-router-dom";

const BlogsList = () => {
  // styles
  const blogStyle = {
    padding: 5,
    border: "solid",
    borderWidth: 1,
    marginBottom: 5,
  };

  // get all blogs
  const { isLoading, isError, data: blogsData, error } = useQuery({
    queryKey: ['blogs'],
    queryFn: blogService.getAllBlogs,
    select: (blogs) => blogs.sort((a, b) => b.likes - a.likes),
    retry: false,
  });
  if (isLoading) {
    return <div>loading data...</div>;
  }
  if (isError) {
    return (
      <>
        <div>blogs service not available due to: {error.message}.</div>
        <div>Unable to get all blogs</div>
      </>
    );
  }
  // console.log('blogsData', blogsData);

  return (
    <div>
      <ul style={{ listStyleType: "none", padding: 0 }}>
        {blogsData.map((blog) => (
          <li key={blog.id} style={blogStyle} className="blog">
            <Link to={`/blogs/${blog.id}`}>{blog.title}</Link> - <span>{blog.author}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default BlogsList;

Solution

  • useQueryClient is not reactive to any query data changing. The standard way is to call useQuery to retrieve any data (which handles caching automatically).

    const allBlogs = useQuery({
        queryKey: ['blogs'],
        queryFn: () => {/* make request to get blog data*/}
    }).data;