文章
next js DataFetching 学习笔记 01
客户端组件获取数据
客户端组件使用useEffect请求API获取数据
"use client";
import { useState, useEffect } from "react";
type User = {
id: number;
name: string;
username: string;
email: string;
phone: string;
};
export default function UsersClient() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
async function fetchUsers() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!response.ok) throw new Error("Failed to fetch users");
const data = await response.json();
setUsers(data);
} catch (err) {
if (err instanceof Error) {
setError(err.message);
} else {
setError("An unknown error occurred");
}
} finally {
setLoading(false);
}
}
fetchUsers();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>{error}</div>;
return (
<ul className="space-y-4 p-4">
{users.map((user) => (
<li
key={user.id}
className="p-4 bg-white shadow-md rounded-lg text-gray-700"
>
<div className="font-bold">{user.name}</div>
<div className="text-sm">
<div>Username: {user.username}</div>
<div>Email: {user.email}</div>
<div>Phone: {user.phone}</div>
</div>
</li>
))}
</ul>
);
}
使用三个组件维护用户状态
- users用于存储获取的用户
- loading用于跟踪是否正在获取数据
- error用于存储错误消息
在useEffect内部使用async异步函数获取接口数据,组件挂载时就在useEffect中立即调用此函数(fetchUsers)
服务端组件获取数据
page.tsx
type User = {
id: number;
name: string;
username: string;
email: string;
phone: string;
};
export default async function UsersServer() {
await new Promise((resolve) => setTimeout(resolve, 2000));
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users: User[] = await response.json();
console.log(users);
return (
<ul className="space-y-4 p-4">
{users.map((user) => (
<li
key={user.id}
className="p-4 bg-white shadow-md rounded-lg text-gray-700"
>
<div className="font-bold">{user.name}</div>
<div className="text-sm">
<div>Username: {user.username}</div>
<div>Email: {user.email}</div>
<div>Phone: {user.phone}</div>
</div>
</li>
))}
</ul>
);
}
loading.tsx
export default function LoadingPage() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-white"></div>
</div>
);
}
error.tsx
"use client";
import { useEffect } from "react";
export default function ErrorPage({ error }: { error: Error }) {
useEffect(() => {
console.error(`${error}`);
}, [error]);
return (
<div className="flex items-center justify-center h-screen">
<div className="text-2xl text-red-500">Error fetching users data</div>
</div>
);
}
同一数据多次网络请求 会去重操作 (Request memoization)
相同的URL和参数的请求进行去重操作,并在同一个渲染传递中重复使用结果

顺序数据获取、并行数据获取
顺序数据获取:请求之间有依赖关系,比如文章列表页面 遍历每个文章 使用userId获取用户信息展示
author.tsx
type User = {
id: number;
name: string;
username: string;
email: string;
};
export async function Author({ userId }: { userId: number }) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const user: User = await response.json();
return (
<div className="text-sm text-gray-500">
Written by:{" "}
<span className="font-semibold text-gray-700 hover:text-gray-900 transition-colors">
{user.name}
</span>
</div>
);
}
page.tsx
import { Suspense } from "react";
import { Author } from "./author";
type Post = {
userId: number;
id: number;
title: string;
body: string;
};
export default async function PostsPage() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts: Post[] = await response.json();
const filteredPosts = posts.filter((post) => post.id % 10 === 1);
return (
<div className="p-4 max-w-7xl mx-auto">
<h1 className="text-3xl font-extrabold mb-8">Blog Posts</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{filteredPosts.map((post) => (
<div key={post.id} className="bg-white shadow-md rounded-lg p-6">
<h2 className="text-2xl font-bold mb-3 text-gray-800 leading-tight">
{post.title}
</h2>
<p className="text-gray-600 mb-4 leading-relaxed">{post.body}</p>
<Suspense
fallback={
<div className="text-sm text-gray-500">Loading author...</div>
}
>
<Author userId={post.userId} />
</Suspense>
</div>
))}
</div>
</div>
);
}
并行查询:根据用户id同时获取用户的文章以及专辑信息
loading.tsx
export default function LoadingPage() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-white"></div>
</div>
);
}
page.tsx
type Post = {
userId: number;
id: number;
title: string;
body: string;
};
type Album = {
userId: number;
id: number;
title: string;
};
async function getUserPosts(userId: string) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`
);
return res.json();
}
async function getUserAlbums(userId: string) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const res = await fetch(
`https://jsonplaceholder.typicode.com/albums?userId=${userId}`
);
return res.json();
}
export default async function Page({
params,
}: {
params: Promise<{ userId: string }>;
}) {
const { userId } = await params;
const postsData = getUserPosts(userId);
const albumsData = getUserAlbums(userId);
const [posts, albums] = await Promise.all([postsData, albumsData]);
return (
<div className="p-4 max-w-7xl mx-auto">
<h1 className="text-3xl font-extrabold mb-8">User Profile</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h2 className="text-2xl font-bold mb-4">Posts</h2>
<div className="space-y-4">
{posts.map((post: Post) => (
<div key={post.id} className="bg-white shadow-md rounded-lg p-6">
<h3 className="text-lg font-bold mb-3 text-gray-800 leading-tight">
{post.title}
</h3>
<p className="text-gray-600 mb-4 leading-relaxed">
{post.body}
</p>
</div>
))}
</div>
</div>
<div>
<h2 className="text-2xl font-bold mb-4">Albums</h2>
<div className="space-y-4">
{albums.map((album: Album) => (
<div key={album.id} className="bg-white shadow-md rounded-lg p-6">
<p className="text-gray-700">{album.title}</p>
</div>
))}
</div>
</div>
</div>
</div>
);
}
这里使用了const [posts, albums] = await Promise.all([postsData, albumsData]); 同时获取数据
并行数据获取,在需要获取多个独立数据块时非常有用
从数据块获取数据
npx prisma init --datasource-provider sqlite
actions操作
/src/actions/product.ts
"use server";
import { addProduct, updateProduct, deleteProduct } from "@/prisma-db";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
export type Errors = {
title?: string;
price?: string;
description?: string;
};
export type FormState = {
errors: Errors;
};
export async function createProduct(prevState: FormState, formData: FormData) {
const title = formData.get("title") as string;
const price = formData.get("price") as string;
const description = formData.get("description") as string;
const errors: Errors = {};
if (!title) {
errors.title = "Title is required";
}
if (!price) {
errors.price = "Price is required";
}
if (!description) {
errors.description = "Description is required";
}
if (Object.keys(errors).length > 0) {
return { errors };
}
await addProduct(title, parseInt(price), description);
redirect("/products-db");
}
export async function editProduct(
id: number,
prevState: FormState,
formData: FormData
) {
const title = formData.get("title") as string;
const price = formData.get("price") as string;
const description = formData.get("description") as string;
const errors: Errors = {};
if (!title) {
errors.title = "Title is required";
}
if (!price) {
errors.price = "Price is required";
}
if (!description) {
errors.description = "Description is required";
}
if (Object.keys(errors).length > 0) {
return { errors };
}
await updateProduct(id, title, parseInt(price), description);
redirect("/products-db");
}
export async function removeProduct(id: number) {
await deleteProduct(id);
revalidatePath("/products-db");
}
createProduct(prevState: FormState, formData: FormData) 当我们使用useActionState并传递createProduct方法时,该函数会自动接收之前的state作为第一个参数,所以之前的state类型为FormState 当提交空表单时就会看到错误信息了
page.tsx
"use client";
import { FormState, createProduct } from "@/actions/products";
import { Submit } from "@/components/submit";
import { useActionState } from "react";
export default function AddProductPage() {
const initialState: FormState = {
errors: {},
};
const [state, formAction] = useActionState(createProduct, initialState);
return (
<form action={formAction} className="p-4 space-y-4 max-w-96">
<div>
<label className="text-white">
Title
<input
type="text"
className="block w-full p-2 text-black border rounded"
name="title"
/>
</label>
{state.errors.title && (
<p className="text-red-500">{state.errors.title}</p>
)}
</div>
<div>
<label className="text-white">
Price
<input
type="number"
className="block w-full p-2 text-black border rounded"
name="price"
/>
</label>
{state.errors.price && (
<p className="text-red-500">{state.errors.price}</p>
)}
</div>
<div>
<label className="text-white">
Description
<textarea
className="block w-full p-2 text-black border rounded"
name="description"
/>
</label>
{state.errors.description && (
<p className="text-red-500">{state.errors.description}</p>
)}
</div>
{/* <button
type="submit"
className="block w-full p-2 text-white bg-blue-500 rounded disabled:bg-gray-500"
disabled={isPending}
>
Submit
</button> */}
<Submit />
</form>
);
}
useActionState记录表单状态:参数1:server action函数名 参数2:初始form state
返回state、formAction、isPending
/src/components/submit.tsx
"use client";
import { useFormStatus } from "react-dom";
export const Submit = () => {
const { pending } = useFormStatus();
return (
<button
type="submit"
className="block w-full p-2 text-white bg-blue-500 rounded disabled:bg-gray-500"
disabled={pending}
>
Submit
</button>
);
};
useFormStatus 返回值:
- pending 是否提交完成
- data 表单提交的数据
- method 请求方法 GET、POST
- action form表单的action属性值
Pending (useFormStatus) 与 isPending (useActionSate) 都能帮我们判断是否正在提交
- 构建重复使用的组件时 Pending更合适
- isPending可以用在任何action中