Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Modern Full-Stack React Projects

You're reading from   Modern Full-Stack React Projects Build, maintain, and deploy modern web apps using MongoDB, Express, React, and Node.js

Arrow left icon
Product type Paperback
Published in Jun 2024
Publisher Packt
ISBN-13 9781837637959
Length 506 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Daniel Bugl Daniel Bugl
Author Profile Icon Daniel Bugl
Daniel Bugl
Arrow right icon
View More author details
Toc

Table of Contents (28) Chapters Close

Preface 1. Part 1:Getting Started with Full-Stack Development
2. Chapter 1: Preparing for Full-Stack Development FREE CHAPTER 3. Chapter 2: Getting to Know Node.js and MongoDB 4. Part 2:Building and Deploying Our First Full-Stack Application with a REST API
5. Chapter 3: Implementing a Backend Using Express, Mongoose ODM, and Jest 6. Chapter 4: Integrating a Frontend Using React and TanStack Query 7. Chapter 5: Deploying the Application with Docker and CI/CD 8. Part 3:Practicing Development of Full-Stack Web Applications
9. Chapter 6: Adding Authentication with JWT 10. Chapter 7: Improving the Load Time Using Server-Side Rendering 11. Chapter 8: Making Sure Customers Find You with Search Engine Optimization 12. Chapter 9: Implementing End-to-End Tests Using Playwright 13. Chapter 10: Aggregating and Visualizing Statistics Using MongoDB and Victory 14. Chapter 11: Building a Backend with a GraphQL API 15. Chapter 12: Interfacing with GraphQL on the Frontend Using Apollo Client 16. Part 4:Exploring an Event-Based Full-Stack Architecture
17. Chapter 13: Building an Event-Based Backend Using Express and Socket.IO 18. Chapter 14: Creating a Frontend to Consume and Send Events 19. Chapter 15: Adding Persistence to Socket.IO Using MongoDB 20. Part 5:Advancing to Enterprise-Ready Full-Stack Applications
21. Chapter 16: Getting Started with Next.js 22. Chapter 17: Introducing React Server Components 23. Chapter 18: Advanced Next.js Concepts and Optimizations 24. Chapter 19: Deploying a Next.js App 25. Chapter 20: Diving Deeper into Full-Stack Development 26. Index 27. Other Books You May Enjoy

Integrating the backend service using TanStack Query

After finishing creating all the UI components, we can now move on to integrating them with the backend we created in the previous chapter. For the integration, we are going to use TanStack Query (previously called React Query), which is a data fetching library that can also help us with caching, synchronizing, and updating data from a backend.

TanStack Query specifically focuses on managing the state of fetched data (server state). While other state management libraries can also deal with server state, they specialize in managing client state instead. Server state has some stark differences from client state, such as the following:

  • Being persisted remotely in a location the client does not control directly
  • Requiring asynchronous APIs to fetch and update state
  • Having to deal with shared ownership, which means that other people can change the state without your knowledge
  • State becoming stale (“out of date”) at some point when changed by the server or other people

These challenges with server state result in issues such as having to cache, deduplicate multiple requests, update “out of date” state in the background, and so on.

TanStack Query provides solutions to these issues out of the box and thus makes dealing with server state simple. You can always combine it with other state management libraries that focus on client state as well. For use cases where the client state essentially just reflects the server state though, TanStack Query on its own can be good enough as a state management solution!

Note

The reason why React Query got renamed to TanStack Query is that the library now also supports other frameworks, such as Solid, Vue, and Svelte!

Now that you know why and how TanStack Query can help us integrate our frontend with the backend, let’s get started using it!

Setting up TanStack Query for React

To set up TanStack Query, we first have to install the dependency and set up a query client. The query client is provided to React through a context and will store information about active requests, cached results, when to periodically re-fetch data, and everything needed for TanStack Query to function.

Let’s get started setting it up now:

  1. Open a new Terminal (do not quit Vite!) and install the @tanstack/react-query dependency by running the following command in the root of our project:
    $ npm install @tanstack/react-query@5.12.2

    We are now going to move our current App component to a new Blog component, as we are going to use the App component for setting up libraries and contexts instead.

  2. Rename the src/App.jsx file to src/Blog.jsx.

    Do not update imports yet. If VS Code asks you to update imports, click No.

  3. Now, in src/Blog.jsx, change the function name from App to Blog:
    export function Blog() {
  4. Create a new src/App.jsx file. In this file, import QueryClient and QueryClientProvider from TanStack React Query:
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
  5. Also, import the Blog component:
    import { Blog } from './Blog.jsx'
  6. Now, create a new query client:
    const queryClient = new QueryClient()
  7. Define the App component and render the Blog component wrapped inside QueryClientProvider:
    export function App() {
      return (
        <QueryClientProvider client={queryClient}>
          <Blog />
        </QueryClientProvider>
      )
    }

That’s all there is to setting up TanStack Query! We can now make use of it inside our Blog component (and its children).

Fetching blog posts

The first thing we should do is fetch the list of blog posts from our backend. Let’s implement that now:

  1. First of all, in the second Terminal window opened (not where Vite is running), run the backend server (do not quit Vite!), as follows:
    $ cd backend/
    $ npm start

    If you get an error, make sure Docker and MongoDB are running properly!

Tip

If you want to develop the backend and frontend at the same time, you can start the backend using npm run dev to make sure it hot reloads when you change the code.

  1. Create a .env file in the root of the project, and enter the following contents into it:
    VITE_BACKEND_URL="http://localhost:3001/api/v1"

    Vite supports dotenv out of the box. All environment variables that should be available to be accessed within the frontend need to be prefixed with VITE_. Here, we set an environment variable to point to our backend server.

  2. Create a new src/api/posts.js file. In this file, we are going to define a function to fetch posts, which accepts the query params for the /posts endpoint as an argument. These query params are used to filter by author and tag and define sorting using sortBy and sortOrder:
    export const getPosts = async (queryParams) => {
  3. Remember that we can use the fetch function to make a request to a server. We need to pass the environment variable to it and add the /posts endpoint. After the path, we add query params, which are prefixed with the ? symbol:
      const res = await fetch(
        `${import.meta.env.VITE_BACKEND_URL}/posts?` +
  4. Now we need to use the URLSearchParams class to turn an object into query params. That class will automatically escape the input for us and turn it into valid query params:
          new URLSearchParams(queryParams),
  5. Like we did before in the browser, we need to parse the response as JSON:
      )
      return await res.json()
    }
  6. Edit src/Blog.jsx and remove the sample posts array:
    const posts = [
      {
        title: 'Full-Stack React Projects',
        contents: "Let's become full-stack developers!",
        author: 'Daniel Bugl',
      },
      { title: 'Hello React!' },
    ]
  7. Also, import the useQuery function from @tanstack/react-query and the getPosts function from our api folder in the src/Blog.jsx file:
    import { useQuery } from '@tanstack/react-query'
    import { PostList } from './components/PostList.jsx'
    import { CreatePost } from './components/CreatePost.jsx'
    import { PostFilter } from './components/PostFilter.jsx'
    import { PostSorting } from './components/PostSorting.jsx'
    import { getPosts } from './api/posts.js'
  8. Inside the Blog component, define a useQuery hook:
    export function Blog() {
      const postsQuery = useQuery({
        queryKey: ['posts'],
        queryFn: () => getPosts(),
      })

    The queryKey is very important in TanStack Query, as it is used to uniquely identify a request, among other things, for caching purposes. Always make sure to use unique query keys. Otherwise, you might see requests not triggering properly.

    For the queryFn option, we just call the getPosts function, without query params for now.

  9. After the useQuery hook, we get the posts from our query and fall back to an empty array if the posts are not loaded yet:
    const posts = postsQuery.data ?? []
  10. Check your browser, and you will see that the posts are now loaded from our backend!

Now that we have successfully fetched blog posts, let’s get the filters and sorting working!

Implementing filters and sorting

To implement filters and sorting, we need to handle some local state and pass it as query params to postsQuery. Let’s do that now:

  1. We start by editing the src/Blog.jsx file and importing the useState hook from React:
    import { useState } from 'react'
  2. Then we add state hooks for the author filter and the sorting options inside the Blog component, before the useQuery hook:
      const [author, setAuthor] = useState('')
      const [sortBy, setSortBy] = useState('createdAt')
      const [sortOrder, setSortOrder] = useState('descending')
  3. Then, we adjust queryKey to contain the query params (so that whenever a query param changes, TanStack Query will re-fetch unless the request is already cached). We also adjust queryFn to call getPosts with the relevant query params:
      const postsQuery = useQuery({
        queryKey: ['posts', { author, sortBy, sortOrder }],
        queryFn: () => getPosts({ author, sortBy, sortOrder }),
      })
  4. Now pass the values and relevant onChange handlers to the filter and sorting components:
          <PostFilter
            field='author'
            value={author}
            onChange={(value) => setAuthor(value)}
          />
          <br />
          <PostSorting
            fields={['createdAt', 'updatedAt']}
            value={sortBy}
            onChange={(value) => setSortBy(value)}
            orderValue={sortOrder}
            onOrderChange={(orderValue) => setSortOrder(orderValue)}
          />

Note

For simplicity’s sake, we are only using state hooks for now. A state management solution or context could make dealing with filters and sorting much easier, especially for larger applications. For our small blog application, it is fine to use state hooks though, as we are focusing mostly on the integration of the backend and frontend.

  1. Now, edit src/components/PostFilter.jsx and add the value and onChange props:
    export function PostFilter({ field, value, onChange }) {
      return (
        <div>
          <label htmlFor={`filter-${field}`}>{field}: </label>
          <input
            type='text'
            name={`filter-${field}`}
            id={`filter-${field}`}
            value={value}
            onChange={(e) => onChange(e.target.value)}
          />
        </div>
      )
    }
    PostFilter.propTypes = {
      field: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired,
      onChange: PropTypes.func.isRequired,
    }
  2. We also do the same for src/components/PostSorting.jsx:
    export function PostSorting({
      fields = [],
      value,
      onChange,
      orderValue,
      onOrderChange,
    }) {
      return (
        <div>
          <label htmlFor='sortBy'>Sort By: </label>
          <select
            name='sortBy'
            id='sortBy'
            value={value}
            onChange={(e) => onChange(e.target.value)}
          >
            {fields.map((field) => (
              <option key={field} value={field}>
                {field}
              </option>
            ))}
          </select>
          {' / '}
          <label htmlFor='sortOrder'>Sort Order: </label>
          <select
            name='sortOrder'
            id='sortOrder'
            value={orderValue}
            onChange={(e) => onOrderChange(e.target.value)}
          >
            <option value={'ascending'}>ascending</option>
            <option value={'descending'}>descending</option>
          </select>
        </div>
      )
    }
    PostSorting.propTypes = {
      fields: PropTypes.arrayOf(PropTypes.string).isRequired,
      value: PropTypes.string.isRequired,
      onChange: PropTypes.func.isRequired,
      orderValue: PropTypes.string.isRequired,
      onOrderChange: PropTypes.func.isRequired,
    }
  3. In your browser, enter Daniel Bugl as the author. You should see TanStack Query re-fetch the posts from the backend as you type, and once a match is found, the backend will return all posts by that author!
  4. After testing it out, make sure to clear the filter again, so that newly created posts are not filtered by the author anymore later on.

Tip

If you do not want to make that many requests to the backend, make sure to use a debouncing state hook, such as useDebounce, and then pass only the debounced value to the query param. If you are interested in gaining further knowledge about the useDebounce hook and other useful hooks, I recommend checking out my book titled Learn React Hooks.

The application should now look as follows, with the posts being filtered by the author entered in the field, and sorted by the selected field, in the selected order:

Figure 4.4 – Our first full-stack application – a frontend fetching posts from a backend!

Figure 4.4 – Our first full-stack application – a frontend fetching posts from a backend!

Now that sorting and filtering are working properly, let’s learn about mutations, which allow us to make requests to the server that change the state of the backend (for example, inserting or updating entries in the database).

Creating new posts

We are now going to implement a feature to create posts. To do this, we need to use the useMutation hook from TanStack Query. While queries are meant to be idempotent (meaning that calling them multiple times should not affect the result), mutations are used to create/update/delete data or perform operations on the server. Let’s get started using mutations to create new posts now:

  1. Edit src/api/posts.js and define a new createPost function, which accepts a post object as an argument:
    export const createPost = async (post) => {
  2. We also make a request to the /posts endpoint, like we did for getPosts:
      const res = await fetch(`${import.meta.env.VITE_BACKEND_URL}/posts`, {
  3. However, now we also set method to a POST request, pass a header to tell the backend that we will be sending a JSON body, and then send our post object as a JSON string:
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(post),
  4. Like with getPosts, we also need to parse the response as JSON:
      })
      return await res.json()
    }

    After defining the createPost API function, let’s use it in the CreatePost component by creating a new mutation hook there.

  5. Edit src/components/CreatePost.jsx and import the useMutation hook from @tanstack/react-query, the useState hook from React, and our createPost API function:
    import { useMutation } from '@tanstack/react-query'
    import { useState } from 'react'
    import { createPost } from '../api/posts.js'
  6. Inside the CreatePost component, define state hooks for title, author, and contents:
      const [title, setTitle] = useState('')
      const [author, setAuthor] = useState('')
      const [contents, setContents] = useState('')
  7. Now, define a mutation hook. Here, we are going to call our createPost function:
      const createPostMutation = useMutation({
        mutationFn: () => createPost({ title, author, contents }),
      })
  8. Next, we are going to define a handleSubmit function, which will prevent the default submit action (which refreshes the page), and instead call .mutate() to execute the mutation:
      const handleSubmit = (e) => {
        e.preventDefault()
        createPostMutation.mutate()
      }
  9. We add the onSubmit handler to our form:
        <form onSubmit={handleSubmit}>
  10. We also add the value and onChange props to our fields, as we did before for the sorting and filters:
          <div>
            <label htmlFor='create-title'>Title: </label>
            <input
              type='text'
              name='create-title'
              id='create-title'
              value={title}
              onChange={(e) => setTitle(e.target.value)}
            />
          </div>
          <br />
          <div>
            <label htmlFor='create-author'>Author: </label>
            <input
              type='text'
              name='create-author'
              id='create-author'
              value={author}
              onChange={(e) => setAuthor(e.target.value)}
            />
          </div>
          <br />
          <textarea
            value={contents}
            onChange={(e) => setContents(e.target.value)}
          />
  11. For the submit button, we make sure it says Creating… instead of Create while we are waiting for the mutation to finish, and we also disable the button if no title was set (as it is required), or if the mutation is currently pending:
          <br />
          <br />
          <input
            type='submit'
            value={createPostMutation.isPending ? 'Creating...' : 'Create'}
            disabled={!title || createPostMutation.isPending}
          />
  12. Lastly, we add a message below the submit button, which will be shown if the mutation is successful:
          {createPostMutation.isSuccess ? (
            <>
              <br />
              Post created successfully!
            </>
          ) : null}
        </form>

Note

In addition to isPending and isSuccess, mutations also return isIdle (when the mutation is idle or in a fresh/reset state) and isError states. The same states can also be accessed from queries, for example, to show a loading animation while posts are fetching.

  1. Now we can try adding a new post, and it seems to work fine, but the post list is not updating automatically, only after a refresh!

The issue is that the query key did not change, so TanStack Query does not refresh the list of posts. However, we also want to refresh the list when a new post is created. Let’s fix that now.

Invalidating queries

To ensure that the post list is refreshed after creating a new post, we need to invalidate the query. We can make use of the query client to do this. Let’s do it now:

  1. Edit src/components/CreatePost.jsx and import the useQueryClient hook:
    import { useMutation, useQueryClient } from '@tanstack/react-query'
  2. Use the query client to invalidate all queries starting with the 'posts' query key. This will work with any query params to the getPosts request, as it matches all queries starting with 'posts' in the array:
      const queryClient = useQueryClient()
      const createPostMutation = useMutation({
        mutationFn: () => createPost({ title, author, contents }),
        onSuccess: () => queryClient.invalidateQueries(['posts']),
      })

Try creating a new post, and you will see that it works now, even with active filters and sorting! As we can see, TanStack Query is great for handling server state with ease.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image