原文发表于我的个人博客.

React Query(现在更名为 TanStack Query)是一个 React 库,用于更轻松地获取和操作服务器端数据。使用 React Query,您可以实现数据与服务器的数据获取、缓存和同步。

在本教程中,您将构建一个简单的 Node.js 服务器,然后学习如何使用 React Query 在 React 网站上与之交互。

请注意,这个版本使用了 React Query 的 v4,现在被命名为 TanStack Query。

您可以在这个 GitHub 存储库中找到本教程的代码。

先决条件

在开始本教程之前,请确保您已安装Node.js。您至少需要版本 14。

服务器设置

在本节中,您将使用 SQLite 数据库设置一个简单的 Node.js 服务器。服务器有 3 个端点来获取、添加和删除笔记。

如果您已经有服务器,则可以跳过此部分并转到网站设置部分。

创建服务器项目

创建一个名为server的新目录,然后使用 NPM 初始化一个新项目:

mkdir server
cd server
npm init -y

进入全屏模式 退出全屏模式

安装依赖

然后,安装开发服务器所需的软件包:

npm i express cors body-parser sqlite3 nodemon

进入全屏模式 退出全屏模式

以下是每个软件包的用途:

1.express使用Express创建服务器。

2.cors是一个 Express 中间件,用于在您的服务器上处理CORS。

3.body-parser是一个 Express 中间件,用于解析请求的正文。

4.sqlite3是 Node.js 的 SQLite 数据库适配器。

5.nodemon是一个库,用于在文件发生新更改时重新启动服务器。

创建服务器

使用以下内容创建文件index.js:

const express = require('express');

const app = express();
const port = 3001;
const cors = require('cors');
const sqlite3 = require('sqlite3').verbose();
const bodyParser = require('body-parser');

app.use(bodyParser.json());
app.use(cors());

app.listen(port, () => {
  console.log(`Notes app listening on port ${port}`);
});

进入全屏模式 退出全屏模式

这使用 Express 在端口3001上初始化服务器。它还使用corsbody-parser中间件。

然后,在package.json中添加一个新脚本start来运行服务器:

  "scripts": {
    "start": "nodemon index.js"
  },

进入全屏模式 退出全屏模式

初始化数据库

index.js之前的app.listen中添加以下代码:

const db = new sqlite3.Database('data.db', (err) => {
  if (err) {
    throw err;
  }

  // create tables if they don't exist
  db.serialize(() => {
    db.run(`CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, 
      created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP)`);
  });
});

进入全屏模式 退出全屏模式

如果文件data.db中不存在,这将创建一个新数据库。然后,如果数据库中不存在notes表,它也会创建它。

添加端点

在数据库代码之后,添加以下代码以添加端点:

app.get('/notes', (req, res) => {
  db.all('SELECT * FROM notes', (err, rows) => {
    if (err) {
      console.error(err);
      return res.status(500).json({ success: false, message: 'An error occurred, please try again later' });
    }

    return res.json({ success: true, data: rows });
  });
});

app.get('/notes/:id', (req, res) => {
  db.get('SELECT * FROM notes WHERE id = ?', req.params.id, (err, row) => {
    if (err) {
      console.error(err);
      return res.status(500).json({ success: false, message: 'An error occurred, please try again later' });
    }

    if (!row) {
      return res.status(404).json({ success: false, message: 'Note does not exist' });
    }

    return res.json({ success: true, data: row });
  });
});

app.post('/notes', (req, res) => {
  const { title, content } = req.body;

  if (!title || !content) {
    return res.status(400).json({ success: false, message: 'title and content are required' });
  }

  db.run('INSERT INTO notes (title, content) VALUES (?, ?)', [title, content], function (err) {
    if (err) {
      console.error(err);
      return res.status(500).json({ success: false, message: 'An error occurred, please try again later' });
    }

    return res.json({
      success: true,
      data: {
        id: this.lastID,
        title,
        content,
      },
    });
  });
});

app.delete('/notes/:id', (req, res) => {
  const { id } = req.params;

  db.get('SELECT * FROM notes WHERE id = ?', [id], (err, row) => {
    if (err) {
      console.error(err);
      return res.status(500).json({ success: false, message: 'An error occurred, please try again later' });
    }

    if (!row) {
      return res.status(404).json({ success: false, message: 'Note does not exist' });
    }

    db.run('DELETE FROM notes WHERE id = ?', [id], (error) => {
      if (error) {
        console.error(error);
        return res.status(500).json({ success: false, message: 'An error occurred, please try again later' });
      }

      return res.json({ success: true, message: 'Note deleted successfully' });
    });
  });
});

进入全屏模式 退出全屏模式

简而言之,这会创建 4 个端点:

1./notes方法的端点GET获取所有笔记。

2./notes/:id方法的端点GET通过 ID 获取笔记。

3./notes端点的方法POST添加注释。

4./notes/:id端点的方法DELETE删除一个笔记。

测试服务器

运行以下命令启动服务器:

npm start

进入全屏模式 退出全屏模式

这将在端口3001上启动服务器。您可以通过向localhost:3001/notes发送请求来测试它。

网站设置

在本节中,您将使用 Create React App (CRA) 创建网站。这是您将使用 React Query 的地方。

创建网站项目

要创建一个新的 React 应用程序,请在不同的目录中运行以下命令:

npx create-react-app website

进入全屏模式 退出全屏模式

这会在目录website中创建一个新的 React 应用程序。

安装依赖

运行以下命令切换到website目录并安装网站所需的依赖项:

cd website
npm i @tanstack/react-query tailwindcss postcss autoprefixer @tailwindcss/typography @heroicons/react @windmill/react-ui

进入全屏模式 退出全屏模式

@tanstack/react-query库是 React Query 库,现在命名为 TanStack Query。其他库是Tailwind CSS相关库,用于为网站添加样式。

顺风 CSS 设置

此部分是可选的,仅用于设置 Tailwind CSS。

使用以下内容创建文件postcss.config.js:

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

进入全屏模式 退出全屏模式

此外,使用以下内容创建文件tailwind.config.js:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/typography')
  ],
}

进入全屏模式 退出全屏模式

然后,使用以下内容创建文件src/index.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

进入全屏模式 退出全屏模式

最后在index.js文件开头导入src/index.css:

import './index.css';

进入全屏模式 退出全屏模式

使用QueryClientProvider

要在所有组件中使用 React Query 客户端,您必须在网站组件层次结构中的较高级别使用它。放置它的最佳位置是src/index.js,它包含了整个网站的组件。

src/index.js文件开头添加以下导入:

import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'

进入全屏模式 退出全屏模式

然后,初始化一个新的 Query 客户端:

const queryClient = new QueryClient()

进入全屏模式 退出全屏模式

最后把传递的参数改成root.render:

root.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

进入全屏模式 退出全屏模式

这包装了App组件,该组件使用QueryClientProvider保存网站的其余组件。此提供程序接受道具client,它是QueryClient的实例。

现在,网站中的所有组件都可以访问用于获取、缓存和操作服务器数据的查询客户端。

执行器显示说明

从服务器获取数据是执行查询的行为。因此,您将在本节中使用useQuery

您将在App组件中显示注释。这些注释是使用/notes端点从服务器获取的。

app.js的内容替换为以下内容:

import { PlusIcon, RefreshIcon } from '@heroicons/react/solid'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'

function App() {
  const { isLoading, isError, data, error } = useQuery(['notes'], fetchNotes)

  function fetchNotes () {
    return fetch('http://localhost:3001/notes')
    .then((response) => response.json())
    .then(({ success, data }) => {
      if (!success) {
        throw new Error ('An error occurred while fetching notes');
      }
      return data;
    })
  }

  return (
    <div className="w-screen h-screen overflow-x-hidden bg-red-400 flex flex-col justify-center items-center">
      <div className='bg-white w-full md:w-1/2 p-5 text-center rounded shadow-md text-gray-800 prose'>
        <h1>Notes</h1>
        {isLoading && <RefreshIcon className="w-10 h-10 animate-spin mx-auto"></RefreshIcon>}
        {isError && <span className='text-red'>{error.message ? error.message : error}</span>}
        {!isLoading && !isError && data && !data.length && <span className='text-red-400'>You have no notes</span>}
        {data && data.length > 0 && data.map((note, index) => (
          <div key={note.id} className={`text-left ${index !== data.length - 1 ? 'border-b pb-2' : ''}`}>
            <h2>{note.title}</h2>
            <p>{note.content}</p>
            <span>
              <button className='link text-gray-400'>Delete</button>
            </span>
          </div>
        ))}
      </div>
      <button className="mt-2 bg-gray-700 hover:bg-gray-600 rounded-full text-white p-3">
        <PlusIcon className='w-5 h-5'></PlusIcon>
      </button>
    </div>
  );
}

export default App;

进入全屏模式 退出全屏模式

以下是此代码片段中发生的简要情况:

1.你用useQuery取笔记。它接受的第一个参数是用于缓存的唯一键。第二个参数是用于获取数据的函数。您将fetchNotes函数传递给它。

2.useQuery返回一个包含多个变量的对象。在这里,您使用其中的 4 个:isLoading是一个布尔值,用于确定当前是否正在获取数据;isError是一个布尔值,用于确定是否发生错误。data是从服务器获取的数据;如果isError为真,则error是错误消息。

3.fetchNotes函数必须返回一个可以解析数据或抛出错误的承诺。在函数中,您向localhost:3001/notes发送GET请求以获取便笺。如果成功获取数据,则在then履行函数中返回。

  1. 在返回的 JSX 中,如果isLoading为真,则显示加载图标。如果isError为真,则会显示一条错误消息。如果成功获取data并且其中包含任何数据,则呈现注释。

  2. 您还显示一个带有加号图标的按钮来添加新笔记。稍后您将实现它。

测试显示注释

要测试到目前为止您已实现的内容,请确保您的服务器仍在运行,然后使用以下命令启动您的 React 应用服务器:

npm start

进入全屏模式 退出全屏模式

默认情况下,这会在localhost:3000上运行您的 React 应用程序。如果您在浏览器中打开它,您首先会看到一个加载图标,然后您将看不到任何注释,因为您还没有添加任何注释。

[React (TanStack) 初学者查询教程](https://res.cloudinary.com/practicaldev/image/fetch/s--Pcza1e0H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https: //backend.shahednasser.com/content/images/2022/07/Screen-Shot-2022-06-28-at-9.10.04-PM.png)

实现添加注释功能

添加注释是对服务器数据的一种变异行为。因此,您将在本节中使用useMutation挂钩。

您将创建一个单独的组件来显示用于添加注释的表单。

使用以下内容创建文件src/form.js:

import { useMutation, useQueryClient } from '@tanstack/react-query'

import { useState } from 'react'

export default function Form ({ isOpen, setIsOpen }) {
  const [title, setTitle] = useState("")
  const [content, setContent] = useState("")
  const queryClient = useQueryClient()

  const mutation = useMutation(insertNote, {
    onSuccess: () => {
      setTitle("")
      setContent("")
    }
  })

  function closeForm (e) {
    e.preventDefault()
    setIsOpen(false)
  }

  function insertNote () {
    return fetch(`http://localhost:3001/notes`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        title,
        content
      })
    })
    .then((response) => response.json())
    .then(({ success, data }) => {
      if (!success) {
        throw new Error("An error occured")
      }

      setIsOpen(false)
      queryClient.setQueriesData('notes', (old) => [...old, data])
    })
  }

  function handleSubmit (e) {
    e.preventDefault()
    mutation.mutate()
  }

  return (
    <div className={`absolute w-full h-full top-0 left-0 z-50 flex justify-center items-center ${!isOpen ? 'hidden' : ''}`}>
      <div className='bg-black opacity-50 absolute w-full h-full top-0 left-0'></div>
      <form className='bg-white w-full md:w-1/2 p-5 rounded shadow-md text-gray-800 prose relative' 
        onSubmit={handleSubmit}>
        <h2 className='text-center'>Add Note</h2>
        {mutation.isError && <span className='block mb-2 text-red-400'>{mutation.error.message ? mutation.error.message : mutation.error}</span>}
        <input type="text" placeholder='Title' className='rounded-sm w-full border px-2' 
          value={title} onChange={(e) => setTitle(e.target.value)} />
        <textarea onChange={(e) => setContent(e.target.value)} 
          className="rounded-sm w-full border px-2 mt-2" placeholder='Content' value={content}></textarea>
        <div>
          <button type="submit" className='mt-2 bg-red-400 hover:bg-red-600 text-white p-3 rounded mr-2 disabled:pointer-events-none' 
            disabled={mutation.isLoading}>
            Add</button>
          <button className='mt-2 bg-gray-700 hover:bg-gray-600 text-white p-3 rounded'
            onClick={closeForm}>Cancel</button>
        </div>
      </form>
    </div>
  )
}

进入全屏模式 退出全屏模式

这是这个表格的简要说明

  1. 此表单用作弹出窗口。它接受isOpensetIsOpen属性来确定表单何时打开并处理关闭它。

  2. 您使用useQueryClient访问查询客户端。这是执行突变所必需的。

  3. 要处理在服务器上添加注释并保持查询客户端中的所有数据同步,您必须使用useMutation挂钩。

4.useMutation钩子接受 2 个参数。第一个是处理突变的函数,在本例中是insertNote。第二个参数是选项的对象。您将一个选项onSuccess传递给它,这是一个在成功执行突变时运行的函数。您可以使用它来重置表单的titlecontent字段。

  1. insertNote中,你向localhost:3001/notes发送POST请求,并在 body 中传入要创建的便笺的titlecontent。如果服务器返回的successbody 参数为false,则抛出错误,表示突变失败。

  2. 如果注释添加成功,则使用queryClient.setQueriesData方法更改notes键的缓存值。此方法接受密钥作为第一个参数,并将与该密钥关联的新数据作为第二个参数。这会更新您网站上使用的所有数据。

  3. 在此组件中,您显示一个包含 2 个字段的表单:titlecontent。在表单中,您使用mutation.isError检查是否发生错误,并使用mutation.error访问错误。

  4. 你在handleSubmit函数中处理表单提交。在这里,您使用mutation.mutate触发突变。这是触发insertNote函数以添加新音符的地方。

然后,在src/app.js文件开头添加以下导入:

import Form from './form'
import { useState } from 'react'

进入全屏模式 退出全屏模式

然后,在组件的开头添加一个新的状态变量来管理表单是否打开:

const [isOpen, setIsOpen] = useState(false)

进入全屏模式 退出全屏模式

接下来,添加一个新函数addNote,它只使用setIsOpen打开表单:

function addNote () {
    setIsOpen(true)
}

进入全屏模式 退出全屏模式

最后,在返回的 JSX 中,将带有加号图标的按钮替换为以下内容:

<button className="mt-2 bg-gray-700 hover:bg-gray-600 rounded-full text-white p-3" onClick={addNote}>
    <PlusIcon className='w-5 h-5'></PlusIcon>
</button>
<Form isOpen={isOpen} setIsOpen={setIsOpen} />

进入全屏模式 退出全屏模式

这会将按钮的onClick处理程序设置为addNote。它还将您之前创建的Form组件添加为App的子组件。

测试加注

如果它们没有运行,请重新运行您的服务器和 React 应用程序。然后,在localhost:3000再次打开该网站。单击加号按钮,将打开一个弹出窗口,其中包含添加新注释的表单。

[React (TanStack) 初学者查询教程](https://res.cloudinary.com/practicaldev/image/fetch/s--C2pleZf3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https: //backend.shahednasser.com/content/images/2022/07/Screen-Shot-2022-07-24-at-6.31.41-PM.png)

输入随机标题和内容,然后单击添加。然后弹出表单将关闭,您可以看到添加的新注释。

[React (TanStack) 初学者查询教程](https://res.cloudinary.com/practicaldev/image/fetch/s--Jt-Lvh8r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/ https://backend.shahednasser.com/content/images/2022/07/Screen-Shot-2022-07-24-at-6.32.53-PM.png)

实施删除笔记功能

您将添加的最后一个功能是删除笔记。删除笔记是另一种变异行为,因为它会操纵服务器的数据。

src/app.js中的App组件的开头添加以下代码:

const queryClient = useQueryClient()
const mutation = useMutation(deleteNote, {
    onSuccess: () => queryClient.invalidateQueries('notes')
})

进入全屏模式 退出全屏模式

在这里,您可以使用useQueryClient访问查询客户端。然后,您使用useMutation创建一个新突变。您将函数deleteNote(接下来将创建)作为第一个参数和选项对象传递给它。

onSuccess选项传递一个做一件事的函数。它执行方法queryClient.invalidateQueries。此方法将特定键的缓存数据标记为过期,这会触发再次检索数据。

因此,一旦删除笔记,您之前创建的执行函数fetchNotes的查询将被触发,并且将再次获取笔记。如果您在网站上创建了使用相同键notes的其他查询,它们也将被触发以更新其数据。

接下来,在同一个文件的App组件中添加函数deleteNote:

function deleteNote (note) {
    return fetch(`http://localhost:3001/notes/${note.id}`, {
      method: 'DELETE'
    })
    .then((response) => response.json())
    .then(({ success, message }) => {
      if (!success) {
        throw new Error(message);
      }

      alert(message);
    })
  }

进入全屏模式 退出全屏模式

该函数接收要删除的note作为参数。它向localhost:3001/notes/:id发送DELETE请求。如果响应的successbody参数为false,则抛出错误。否则,仅显示警报。

然后,在返回的App组件的 JSX 中,将之前显示的加载图标和错误更改为以下内容:

{(isLoading || mutation.isLoading) && <RefreshIcon className="w-10 h-10 animate-spin mx-auto"></RefreshIcon>}
{(isError || mutation.isError) && <span className='text-red'>{error ? (error.message ? error.message : error) : mutation.error.message}</span>}

进入全屏模式 退出全屏模式

这显示了获取笔记的查询和处理删除笔记的突变的加载图标或错误消息。

最后,找到一个便笺的删除按钮,并添加一个onClick处理程序:

<button className='link text-gray-400' onClick={() => mutation.mutate(note)}>Delete</button>

进入全屏模式 退出全屏模式

单击时,使用mutation.mutate触发负责删除注释的突变。您将要删除的注释传递给它,它是map循环中的当前注释。

测试删除注释

如果它们没有运行,请重新运行您的服务器和 React 应用程序。然后,在localhost:3000再次打开该网站。单击任何笔记的删除链接。如果笔记被成功删除,将显示警报。

[React (TanStack) 初学者查询教程](https://res.cloudinary.com/practicaldev/image/fetch/s--uVOSkCH---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https ://backend.shahednasser.com/content/images/2022/07/Screen-Shot-2022-07-01-at-2.48.53-PM.png)

关闭警报后,如果有任何其他笔记,将再次提取并显示笔记。

[React (TanStack) 初学者查询教程](https://res.cloudinary.com/practicaldev/image/fetch/s--4RF7gPU1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https: //backend.shahednasser.com/content/images/2022/07/Screen-Shot-2022-06-28-at-9.10.04-PM-1.png)

结论

使用 React (TanStack) Query,您可以使用高级功能(例如跨 React 应用程序的缓存和同步)轻松处理网站上的服务器数据获取和操作。

请务必查看官方文档以了解有关您可以使用 React Query 做什么的更多信息。

Logo

React社区为您提供最前沿的新闻资讯和知识内容

更多推荐