数据获取
数据获取
数据获取模式
顺序获取和并行
- 顺序数据获取
- 一个组件的请求依赖于另一组件的请求结果;
- 按顺序依次获取,产生瀑布流效应;
- 所有请求完成后显示 UI;
- 并行数据获取
- 同时发起多个请求并同时加载数据;
- 所有请求完成会显示 UI;
避免瀑布流
preload
- 一种 API 风格,不必将 promise 层层传递;
import { getItem } from "@/utils/get-item";
export const preload = (id: string) => {
void getItem(id);
};
export default async function Item({ id }: { id: string }) {
const result = await getItem(id);
// ...
}
import Item, { preload, checkIsAvailable } from "@/components/Item";
export default async function Page({
params: { id },
}: {
params: { id: string };
}) {
// starting loading item data
preload(id);
// perform another asynchronous task
const isAvailable = await checkIsAvailable();
return isAvailable ? <Item id={id} /> : null;
}
fetch
- 使用 fetch api 获取数据;
async function getData() {
const res = await fetch("https://api.example.com/...");
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return res.json();
}
export default async function Page() {
const data = await getData();
return <main></main>;
}
服务器操作
服务器操作
- 使用
use server
指令标记; - 在服务器执行的异步函数;
- 虽标记为服务器操作,但依旧可以在客户端中复用;
服务器组件
- 函数级别或模块级别使用
use server
指令;
// Server Component
export default function Page() {
// Server Action
async function create() {
'use server'
// ...
}
return (
// ...
)
}
客户端组件
客户端组件
- 只能在模块级别使用
use client
指令; - 定义该模块的所有函数为客户端操作;
"use client";
export async function create() {
// ...
}
传递服务器操作
- 标记为服务器操作的函数可以作为属性传递给客户端组件;
"use client";
export default function ClientComponent({ updateItem }) {
return <form action={updateItem}>{/* ... */}</form>;
}
最佳实践
Forms
基本操作
export default function Page() {
async function createInvoice(formData: FormData) {
"use server";
const rawFormData = {
customerId: formData.get("customerId"),
amount: formData.get("amount"),
status: formData.get("status"),
};
// mutate data
// revalidate cache
}
return <form action={createInvoice}>...</form>;
}
传递额外参数
- 使用 bind() 方法;
// client-Component.ts
"use client";
import { updateUser } from "./actions";
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId);
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update User Name</button>
</form>
);
}
// action.ts
("use server");
export async function updateUser(userId, formData) {
// ...
}
错误处理
- 返回错误对象;
- 使用 react 的 useFormStatus hook 处理;
- 实验性行为,只能应用于 ssr,以后再看;
表单状态
- 使用 react 的 useFormStatus hook;
- 获取表单状态;
- 实验性行为,只能应用于 ssr,以后再看;
更新 UI
- 使用 react 的 useOptimistic hook;
- 在服务器操作完成之间更新 UI;
- 实验性行为,只能应用于 ssr,以后再看;
非表单
event handler
- 使用 event handler 触发服务器操作;
"use client";
import { incrementLike } from "./actions";
import { useState } from "react";
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes);
return (
<button
onClick={async () => {
// 触发服务器操作
const updatedLikes = await incrementLike();
setLikes(updatedLikes);
}}
>
Like
</button>
);
}
useEffect
- 使用 useEffect 触发服务器操作;
"use client";
import { incrementViews } from "./actions";
import { useState, useEffect } from "react";
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews);
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews();
setViews(updatedViews);
};
updateViews();
}, []);
return <p>Total Views: {views}</p>;
}
错误处理
- next 一旦报错,被最近层级的 error.js 捕获;
- 推荐使用 try/catch 发送语义性错误;
"use server";
export async function createTodo(prevState: any, formData: FormData) {
try {
// Mutate data
} catch (e) {
throw new Error("Failed to create task");
}
}
安全操作
统一秘钥
- 使用 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY 同一配置秘钥;
CSRF
- next 统一使用 POST 方法避免 CSRF 攻击;
- 额外将 Origin 和 Host 比较,保证同一主机才可以调用服务器操作;
- 可使用 serverActions.allowedOrigins 配置安全来源数组;
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ["my-proxy.com"],
},
},
};
敏感数据
- 使用 react 的 taintObjectReference 和 taintUniqueValue hook;
- 避免敏感值传递给客户端;
- 实验性行为,只能应用于 ssr,以后再学;