react课程19-ReactQuery

Last updated on September 24, 2024 pm

本节课学习能够远程管理supabase的React Query🤓

(Jonas终于能教他最爱的库了,他很激动🙉)

一·、什么是React Query

(1)特点

image-20240922103715683image-20240922103801784

✅数据获取:提供简洁的 API 来发起异步请求,支持 REST API、GraphQL 等数据源。

缓存管理:自动缓存数据,避免不必要的网络请求,提高性能。

实时更新:支持实时数据更新,通过轮询、WebSocket 或后台刷新机制来保持数据的新鲜度。

✅自动重试:当请求失败时,可以自动重试请求,增强应用的稳定性。

✅查询和变更:使用 useQuery 来获取数据,使用 useMutation 来处理数据的增删改操作。

状态管理:提供丰富的状态管理,允许开发者轻松处理加载、错误和成功状态。

✅易于集成:可以与现有的 React 应用轻松集成,无需重构代码。

(2)与其他类似的库相比的优点

React Query 相对于其他类似库(如 Redux、Apollo Client、SWR 等)有几个独特的优势和特点:

  1. 数据获取与缓存分离

React Query 专注于数据获取和缓存管理,而不是全局状态管理。这使得它更加简洁和高效,专注于异步数据的流动,而不需要管理其他状态。

  1. 自动缓存和无缝更新

React Query 会自动缓存请求的数据,并在数据过期时自动重新获取。它提供了强大的数据同步机制,允许实时更新和后台刷新,保持数据的新鲜度。

  1. 轻量级的 API

与 Redux 等库相比,React Query 提供了更简单的 API。使用 useQueryuseMutation 进行数据操作,无需编写复杂的 reducer 和 action。

  1. 内置的状态管理

React Query 内置了加载、错误和成功等状态管理,无需额外处理。开发者可以轻松获取请求状态,从而更好地处理 UI 渲染。

  1. 自动重试和错误处理

它可以自动重试失败的请求,并允许开发者自定义重试逻辑。这种机制提高了应用的鲁棒性。

  1. 无缝集成

React Query 可以与任何后端 API(REST、GraphQL 等)无缝集成,无需特定的配置。这种灵活性使得它适用于各种项目。

  1. 支持服务端渲染(SSR)

React Query 提供了服务端渲染支持,允许在服务器端预取数据,并在客户端使用,提升页面加载性能。

  1. 开箱即用的 DevTools

React Query 提供了开发者工具,方便查看当前的查询状态、缓存和数据。这对于调试和优化应用非常有帮助。

二、set up React Query✅

npm i date-fns 日期功能(跟视频不一样了,导入方式,问GPT吧)

(1)安装配置

npm i @tanstack/react-query@4 https://tanstack.com/query/v3

(23年是v4,现在已经是v5了🙉)

npm i @tanstack/react-query-devtools@4安装Dev Tool(这次不用浏览器内安装)

(2)初步使用

在App.js中写入const queryClient = new QueryClient({(可选项)})

<QueryClientProvider client={queryClient}>包裹整个App

子组件中写入<ReactQueryDevtools initialIsOpen={false} />

(3)示例

获取数据的方式:

1
2
3
4
5
6
7
8
const {
isLoading,
data: cabins,
error,
} = useQuery({
queryKey: ['cabins'],
queryFn: getCabins,
});

设置数据由fresh到stale的时间:(在App.js)

1
2
3
4
5
6
7
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 0,
},
},
});

getCabins:(从API Docs中查看相应代码)(记得修改相应Policy权限)

1
2
3
4
5
6
7
export async function getCabins() {
const { data, error } = await supabase.from('cabins').select('*');
if (error) {
throw new Error('cabins could not be loaded');
}
return data;
}

三、mutating(变异)数据

当在supabase中修改数据后,网站上加载出的表单会跟随staleTime改变。

但是如果是远程修改数据呢?(比如删除,如下所示)

(1)删除方程(放置在apiCabins中)

修改了eq()中的内容,可以和Docs中的代码做照应

1
2
3
4
5
6
7
export async function deleteCabin({ id }) {
const { data, error } = await supabase.from('cabins').delete().eq('id', id);
if (error) {
throw new Error('cabins could not be deleted');
}
return data;
}

第一个值 'id'

  • 这个值是数据库表中的列名,表示你要匹配的字段。在这里,它指的是小屋表中的 id 列。

第二个值 id

  • 这个值是你想删除的具体记录的唯一标识符。它是从传入的对象中解构出来的 id 值,代表要删除的小屋的 ID。

(2)连接删除组件

要点解析在代码注释中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function CabinRow({ cabin }) {
const {
id: cabinId, //这里重构了名称,因此在onClick函数中不能只传递一个参数而是需要传递对象
name,
maxCapacity,
regularPrice,
discount,
image,
} = cabin;

const queryClient = useQueryClient(); //通过这个自定义hook来获取App.Js中建立的Client

const { isLoading: isDeleting, mutate } = useMutation({ //看这个hook!!!!
mutationFn: deleteCabin, //这是变异数据的方程
onSuccess: () => { //这是编译成功的回调函数
alert('Cabin successfully deleted');
queryClient.invalidateQueries({
queryKey: ['cabins'],
}); //这里的目的是强制触发重新获取数据以更新界面,invalidate的意思就是更新
},
onError: (err) => alert(err.message), //这里是失败后的回调函数,显示错误
});

return (
<TableRow role="row">
<Img src={image} />
<Cabin>{name}</Cabin>
<div>Fits up to {maxCapacity} guests</div>
<Price>{formatCurrency(regularPrice)}</Price>
<Discount>{formatCurrency(discount)}</Discount>
<button onClick={() => mutate({ id: cabinId })} disabled={isDeleting}>
delete {/*就是这里的函数,参数使用{id:cabinId}而不是直接是cabinId*/}
</button>
</TableRow>
);
}

(3)装饰通知(React Toast)

npm i react-hot-toast https://react-hot-toast.com/(好可爱的界面hhh)

image-20240922164720981

image-20240922164746787

1、首先:(先去查看官方文档)在App.js的架构中,在子组件的最后写上:

(烤面包机当通知也太好笑了吧hhhh)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<Toaster
position="top-center" //位置
gutter={12} //各个toast的间距
containerStyle={{ margin: '8px' }} //外边距
toastOptions={{ //定义了不同类型 toast 的显示选项。
success: {
duration: 3000//持续时间
},
error: {
duration: 5000,
},
style: {
fontSize: '16px',
maxWidth: '500px',
padding: '16px 24px',
backgroundColor: 'var(--color-grey-0)',
color: 'var(--color-grey-700)',
},
}}
/>
2、使用
1
toast.success('Cabin successfully deleted');

四、React Hook Form

npm i react-hook-form@7

(1)入门

非常好的库,使提交表格方便。

1、const { register, handleSubmit } = useForm();
  • **register**:这是一个函数,用于将表单字段注册到 React Hook Form 中,以便它能够跟踪其状态和验证。
  • **handleSubmit**:这是一个函数,用于处理表单提交事件。当用户提交表单时,它会调用你提供的提交处理函数。
2、在input字段中加入register:
1
<Input type="text" id="name" {...register('name')} />
3、添加提交函数

这是我们自己创建的Form组件,添加onSubmit函数:

1
<Form onSubmit={handleSubmit(onSubmit)}>  

这里是onSubmit函数:(然后表格里的数据就完美出现了)

1
2
3
function onSubmit(data) {
console.log(data);
}

下面是被register后的input字段的prop,多了不一样的东西。

image-20240922182919583

4、使用Form创建新的Cabin
1
2
3
4
5
6
7
8
9
10
export async function createCabin(newCabin) {
const { data, error } = await supabase
.from('cabins')
.insert([newCabin])
.select();
if (error) {
throw new Error('cabins could not be created');
}
return data;
}

其实没啥特别的,注意表单的内容和cabin的结构一致就行。

(2)表单的错误处理

”React Hook Form最闪亮耀眼的地方是表单错误验证“

1、

先看看我们通过useForm获得了什么:

const { register, handleSubmit, reset, getValues, formState } = useForm();

  • **reset**:用于重置表单的值到初始状态,通常在提交成功后调用。
  • **getValues**:获取当前表单字段的值,可以在需要时获取字段的最新值。
  • **formState**:包含表单的状态信息,如 isSubmittingisValiderrors 等,方便进行表单状态的管理和展示。

于是……

  • 可以在onSuccess的回调函数中添加 reset(),清空表单
  • const { errors } = formState;来获得错误信息以显示在ui界面,Form的onSubmit函数中添加onError: <Form onSubmit={handleSubmit(onSubmit, onError)}>,如果表单提交不成功就会调用onError函数,它的参数是errors
2、表单验证

验证内容在register中添加,例如

  • maxCapacity表单:
1
2
3
4
5
6
7
{...register('maxCapacity', {
required: 'This field is requied',
min: {
value: 1,
message: 'Capacity should be at least 1',
},
})}
  • discount表单:(validate关键字:自定义验证)✅应用getValues函数访问表内数据
1
2
3
4
5
6
{...register('discount', {
required: 'This field is requied',
validate: (value) =>
value <= getValues().regularPrice ||
'DisCount should be less than regular price ',
})}

五、上传图片到supabase

https://supabase.com/docs/reference/javascript/storage-from-upload访问此网站找寻详细信息

首先需要设置rlp(Policy)

(1)建立函数

1、得到图片名和图片路径
1
const imageName = `${Math.random()}-${newCabin.image.name}`.replaceAll('/', '');

使用 Math.random() 生成随机数,以防止图片名称冲突,并将其与原图片名称拼接。replaceAll 用于去除 / 字符。

1
const imagePath = `${supabaseUrl}/storage/v1/object/public/cabin-images/${imageName}`;

supabaseUrl是在supabase.js中定义的。至于路径,我们在前面创建了两个Buket,其中一个是cabin-images,存储cabin图像,从那里复制而来。

2、create cabin
1
2
3
4
5
6
7
const { data, error } = await supabase
.from('cabins')
.insert([{ ...newCabin, image: imagePath }]);

if (error) {
throw new Error('Cabin could not be created');
}

❓❓在这段代码中,将 image 单独列出来的原因主要有以下几点:

  1. 存储路径的动态性

    • 图片的路径是通过生成随机名称和构造完整 URL 动态创建的。在插入到数据库之前,路径需要先确定。因此,在插入数据时,使用生成的 imagePath 作为字段的一部分。
  2. 解耦数据逻辑

    • image 单独列出可以清晰地分隔小屋的基本信息和图片信息,使数据结构更清晰。在数据表中,通常不直接存储文件的内容,而是存储文件的 URL 或路径。
  3. 确保数据完整性

    • 在创建小屋的过程中,首先插入小屋信息,再上传图片。如果上传失败,可以根据存储的 id 删除对应的小屋记录。这种分步骤处理的方式使得在发生错误时能够更容易地回滚操作。
  4. 方便后续处理

    • 如果将 image 与其他小屋信息合并在一起,后续处理时可能会变得复杂。例如,如果需要仅更新或删除图片,单独存储可以更加方便。
3、upload image
1
2
3
const { error: storageError } = await supabase.storage
.from('cabin-images') //这里写buket的名字
.upload(imageName, newCabin.image); //括号前面写文件名,后面是上传的文件本身
4、Delete the cabin if there was an error uploading image
1
2
3
4
5
6
7
if (storageError) {
await supabase.from('cabins').delete().eq('id', data.id);
console.log(storageError);
throw new Error(
'Cabin image could not be uploaded and the cabin was not created'
);
}

(2)修改submit函数

1
2
3
function onSubmit(data) {
mutate({ ...data, image: data.image[0] });
}

六、修改cabin信息

改bug改的想鼠。。。💀💀💀💀💀💀(把Jonas没找到的bug找出来了,我真棒)

这一部分有点儿难理解,让我仔细地捋一下。

(1)传递表单原有信息

在修改信息的时候,很明显我们是需要原有的信息的,然后在此基础上加以修改。由于这个修改信息的表单应该和创建信息的表单一样,所以我们可以偷懒直接借用。于是在CabinRow这个组件里,我们可以添加一个按钮:edit,onClick函数控制showForm状态变量,然后就可以在cabinRow的要修改的那一行下面显示CreateCabinForm组件了。(传递给它cabin的所有信息)

1
{showForm && <CreateCabinForm cabinToEdit={cabin} />}

(2)改变CreateCabinForm

1、接收原有的cabin信息

组件函数接收object prop:{ cabinToEdit = {} }

={}是给它一个default值,因为创建cabin的时候没有传递cabinToEdit,所以就是空。

2、运用原有的cabin信息
  • 首先进行拆解:const { id: editId, ...editValues } = cabinToEdit;

这里把id重命名为editId是为了下面传递参数更加方便。

  • 然后定义变量:const isEditSession = Boolean(editId);

没有editId说明是创建cabin,这个变量决定了在mutate数据的时候究竟是创建cabin还是修改cabin,因为两个动作接受的参数也不愿意。

  • 接着给予表单defaultValues:const { register, handleSubmit, reset, getValues, formState } = useForm({defaultValues: isEditSession ? editValues : {}, });

这里也比较好理解,如果不是编辑默认值就为空。

3、创建mutate

这里跟createCabin的逻辑很像,只是改变了名字和接收的参数而已。

mutationFn: ({ newCabinData, id }) => createEditCabin(newCabinData, id)其中这一行着重看。

(⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️请注意请注意这里onSuccess后不能reset()!!!否则image就变成空(undefined)的了,不知道为什么,其他数据都没问题。。。)

判断失误,汗流浃背了,原来只是因为api里那个函数写错了,有个小bug,startsWith而不是startWith。。。真的汗流浃背了,我就说Jonas那里怎么没问题。。但是我真的很奇怪为什么在onSubmit函数里console.log(image)为什么是undifined。。

1
2
3
4
5
6
7
8
9
10
const { mutate: editCabin, isLoading: isEditing } = useMutation({
mutationFn: ({ newCabinData, id }) => createEditCabin(newCabinData, id),
onSuccess: () => {
toast.success('Cabin successfully edited');
queryClient.invalidateQueries({
queryKey: ['cabins'],
});
},
onError: (err) => toast.error(err.message),
});
4、修改表单disabled逻辑

const isWorking = isCreating || isEditing;用新变量处理

5、修改onSubmit函数

如果图片是新上传的,那么它应该是一个对象,我们需要得到得到的是它的名字来给它建立url,如果是旧的信息,那么它应该是string,直接返回它本身就好。

这里两个函数的参数不太一样,可以观察一下。

1
2
3
4
5
6
7
8
9
function onSubmit(data) {
// console.log(data);
const image = typeof data.image === 'string' ? data.image : data.image[0];
// console.log(image);

if (isEditSession)
editCabin({ newCabinData: { ...data, image }, id: editId });
else createCabin({ ...data, image: image });
}

(3)创建、修改Cabin函数合二为一

从上到下讲一下要点:

1、通过一个很巧妙的函数来判定图片需不需要重新创建Url

接收到传过来的数据时,如果image是旧的,那么它的开头应该是supabaseUrl,所以通过这个来判断是沿用这个image还是通过它新建Url。

2、if()条件语句下,需要换一种方式接收数据

await supabase.from('cabins') 本身是一个查询操作,返回的是一个查询对象。如果直接在 if 语句中使用它,可能导致控制流不清晰。你需要先构建查询,再执行它。所以我们在这里先创建了query来接收数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
export async function createEditCabin(newCabin, id) {
// console.log(newCabin, id);
const hasImagePath = newCabin.image?.startsWith?.(supabaseUrl);

const imageName = `${Math.random()}-${newCabin.image.name}`.replaceAll(
'/',
''
);
const imagePath = hasImagePath
? newCabin.image
: `${supabaseUrl}/storage/v1/object/public/cabin-images/${imageName}`;

//1.create/edit cabin
let query = supabase.from('cabins');

//a)create
if (!id) query = query.insert([{ ...newCabin, image: imagePath }]);

//b)edit
if (id) query = query.update({ ...newCabin, image: imagePath }).eq('id', id);

const { data, error } = await query.select().single();

if (error) {
throw new Error('Cabin could not be created');
}

//2.upload image
const { error: storageError } = await supabase.storage
.from('cabin-images')
.upload(imageName, newCabin.image);

//3.Delete the cabin if there was an error uploading image
if (storageError) {
await supabase.from('cabins').delete().eq('id', data.id);
console.log(storageError);
throw new Error(
'Cabin image could not be uploaded and the cabin was not created'
);
}

return data;
}

七、复制cabins

(1)创建custom hook

一共创建了四个:

  • useCabins:得到cabin数据
  • useCreateCabin:得到创建cabin的函数
  • useDeleteCabin:得到删除cabin的函数
  • useEditCabin:得到修改cabin的函数

reset()函数不能放在这些文件里,但是可以放在onSubmit中,在调用mutate函数时通过onSuccess当作参数传递

(2)复制Cabin

只需要创建一个新的按钮,把onClick函数写为creatCabin,传递的参数就是cabinRow中的cabin信息,把名字更改为copy of ${name}即可。

八、修改设置

由于只有一行设置,因此对它的改变比较简单。下面只讲一下要点。

下面是其中一个修改框:

1
2
3
4
5
6
7
8
9
<FormRow label="Minimum nights/booking">
<Input
type="number"
id="min-nights"
disabled={isUpdating}
defaultValue={minBookingLength}
onBlur={(e) => handleUpdate(e, 'minBookingLength')}
/>
</FormRow>

onBlur(失去焦点)时触发 handleUpdate,并将用户输入的值与字段名 'minBookingLength' 一起传递给 handleUpdate 函数。

下面是handleUpdate函数:

1
2
3
4
5
 function handleUpdate(e, field) {
const { value } = e.target; //解构赋值
if (!value) return;
updateSetting({ [field]: value });
}

九、零碎

(1)滚动条

1、使得sidebar固定,只有main的部分滑动

在Main的样式中加入: overflow: scroll;

2、隐藏全局滚动条(真的很丑)

GlobalStyles.js中找到/新建一个body元素,写入: overflow: hidden;

(2)children的prop也可以访问

例如我们会创建cabin表新建的可重用组件:

1
2
3
4
5
6
7
8
9
function FormRow({ label, error, children }) {
return (
<StyledFormRow>
{label && <Label htmlFor={children.props.id}>{label}</Label>}
{children}
{error && <Error>{error}</Error>}
</StyledFormRow>
);
}

(3)连接起来的两个表单不能直接删除元素哦

{ “code”: “23503”, “details”: “Key is still referenced from table "booking".”, “hint”: null, “message”: “update or delete on table "cabins" violates foreign key constraint "booking_cabinId_fkey" on table "booking"“ }

想要删除一个cabin的时候报错了,因为它和另一张表单(booking)的内容连接起来了。

9/24:终于完成这一节了呜呜呜呜呜,真不容易😭😭😭😭😭😭😭😭😭😭😭😭😭😭


react课程19-ReactQuery
http://example.com/2024/09/22/react课程19-ReactQuery/
Author
Yaodeer
Posted on
September 22, 2024
Licensed under