使用社交账号登录
Next.js 的核心竞争力在于极其灵活的混合渲染机制。理解每种模式的 触发时机、数据流向 和 适用场景 是架构选型的基础。
| 渲染模式 | 简述 | 构建/执行时机 | 数据新鲜度 | 典型场景 |
|---|---|---|---|---|
| SSG (Static Site Generation) | 纯静态生成 | npm run build 时 | 低 (下次构建前不变) | 营销页、文档、帮助中心 |
| ISR (Incremental Static Regeneration) | 增量静态再生 | 构建时 + 运行时按需 (Revalidation) | 中 (可配置秒级更新) | 电商列表页、新闻资讯 |
| SSR (Server-Side Rendering) | 服务端渲染 | 每次请求 (Request Time) | 高 (实时) | 个人中心、即时数据看板 |
| CSR (Client-Side Rendering) | 客户端渲染 | 浏览器运行时 | 高 (依赖 API 延迟) | 管理后台 dashboard、复杂交互页 |
Next.js 13+ 引入的 App Router 彻底改变了组件模型,核心在于 React Server Components (RSC)。
默认情况下,app 目录下的组件都是 RSC。
useState, useEffect, 无法绑定 DOM 事件 (onClick)。实战:在 Server Component 中获取数据
Next.js 扩展了 fetch API,并支持在组件中直接使用 async/await。
当且仅当你在文件顶部声明 'use client' 时,该组件及其导入的所有子组件才会成为 Client Component。
为了最大化 RSC 的优势,应尽量将 Client Component 推向组件树的末端(叶子节点)。
水合是现代 React/Next.js 应用中最关键但也最容易被误解的概念。
简单来说,水合是 React "接管" 服务端生成的静态 HTML 的过程。
当服务端生成的 HTML 与客户端初次渲染的 HTML 不一致时,React 会抛出 Hydration Error,并强制客户端重新渲染整个节点,导致性能损耗和 UI 闪烁。
典型错误场景:
tsx
// ❌ 服务端生成的时间与客户端渲染时的时间不一致
<div>{new Date().toLocaleTimeString()}</div>
<div>{Math.random()}</div>
tsx
// ❌ 服务端没有 window 对象,渲染出不同内容
<div>{typeof window !== 'undefined' ? 'Client' : 'Server'}</div>
<p> 标签里嵌套 <div>(这是非法的 HTML,浏览器会自动修复 DOM 结构,导致 React 找不到预期的节点)。使用 useEffect 延迟渲染:确保逻辑只在客户端执行。
suppressHydrationWarning:如果你明确知道内容会不一致(如时间戳),可以强制 React 忽略警告。
Hooks 也就是 React 的"魔法",但如果不理解其背后的 引用稳定性 (Referential Equality) 原理,极易导致性能灾难。
很多开发者认为 useMemo 只是为了缓存昂贵的计算结果,其实它更重要的作用是 保持引用稳定。
useMemo 本身有开销。对于简单的 a + b 或者简单的字符串拼接,使用 useMemo 是负优化。useCallback 是 useMemo 的函数特化版本。
React.memo 优化了,但 props 里的函数每次父组件渲染都变,优化就会失效。这是 useCallback (以及 useEffect) 最容易出错的地方。
useEffect 里。useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。修改 .current 不会触发组件重新渲染。
this.xxx 实例变量。
签名与 useEffect 相同,但它会在所有的 DOM 变更之后 同步 调用。
useEffect,用户可能会先看到初始布局,然后瞬间跳变到新布局。useLayoutEffect 会阻塞浏览器绘制,直到执行完毕,确保用户只看到最终状态。应当尽量避免使用。但在某些场景(如封装复杂的 UI 库组件)下,你可能需要向父组件暴露特定的方法(如 focus(), scrollToBottom()),而不是整个 DOM 节点。
React 18 引入了并发渲染,带来了提升用户体验的新 Hooks。
用于将某些状态更新标记为 "Transition" (非紧急更新)。
useTransition 是包装更新动作,useDeferredValue 是包装状态值本身。类似于防抖 (Debounce),但更智能(由 React 调度决定延迟多久)。
随着应用复杂度增加,useState 往往力不从心,而引入 Redux 又显得过重。
这种模式适合管理中等复杂度的全局状态(如主题、用户信息、购物车)。
Context 有一个著名的性能问题:Provider 更新时,所有 Consumer 都会重渲染,即使它们只使用了 State 的一部分。
优化策略:读写分离
将 State (数据) 和 Dispatch (修改方法) 分离到两个不同的 Context 中。
在引入全局状态管理前,先问自己:这个状态真的需要全局吗? 将状态尽可能的下放到离使用它最近的父组件,甚至是组件内部,是提升 React 应用性能最简单有效的方法。
❌ 错误示范:
✅ 正确示范:
当看到 <Child user={user} /> -> <GrandChild user={user} /> -> <GreatGrandChild user={user} /> 时,你需要重构。
解法 1:Component Composition (组件组合)
将组件作为 children 或 props 传递,而不是传递数据。
解法 2:Context 如果组件层级确实太深且无法组合,再考虑 Context,比如主题切换、用户信息等。
通过避免这些反模式,并结合前文提到的渲染策略与 Hooks 技巧,你已经掌握了构建高质量 React 应用的核心心法。
// app/page.tsx (默认是 Server Component)
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // ISR: 每小时重新验证一次
});
if (!res.ok) throw new Error('Failed to fetch data');
return res.json();
}
export default async function Page() {
const data = await getData(); // 直接 await,像写后端代码一样简单
return (
<main>
<h1>{data.title}</h1>
<p>{data.content}</p>
</main>
);
}
// ❌ 错误做法:在根布局就把整个应用变成 Client Component
// app/layout.tsx
'use client'
export default function RootLayout({ children }) { ... }
// ✅ 正确做法:将交互逻辑隔离在独立组件中
// app/layout.tsx (Server Component)
import { SearchBar } from './SearchBar'; // SearchBar 是 Client Component
export default function RootLayout({ children }) {
return (
<html>
<body>
<nav><SearchBar /></nav> {/* 只有 SearchBar 会打包 JS 发送给浏览器 */}
<main>{children}</main>
</body>
</html>
);
}
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null; // 或返回加载占位符
return <div>{window.innerWidth}</div>;
<time suppressHydrationWarning>{new Date().toLocaleTimeString()}</time>
// ❌ 反例:每次父组件渲染,filterOptions 都会生成新的对象引用
const Parent = () => {
const filterOptions = { role: 'admin', status: 'active' };
// 导致 Child 即使被 React.memo 包裹也会重渲染
// 导致 useEffect 即使逻辑没变也会再次触发
return <Child options={filterOptions} />;
};
// ✅ 正确:锁定引用
const Parent = () => {
const filterOptions = useMemo(() => ({
role: 'admin', status: 'active'
}), []); // 依赖为空,整个生命周期引用不变
return <Child options={filterOptions} />;
};
const [count, setCount] = useState(0);
// ❌ 陷阱:这个函数创建时捕获了当时的 count (比如 0)
// 如果依赖数组为空 [],无论后续 count 变成多少,log 出来的永远是 0
const handleLog = useCallback(() => {
console.log('Current count:', count);
}, []); // 缺少 count 依赖
// ✅ 修正方案 A:添加依赖
const handleLogFixed = useCallback(() => {
console.log('Current count:', count);
}, [count]); // 每次 count 变了,生成新函数(这有时会违背初衷)
// ✅ 修正方案 B:使用 Ref 穿透闭包 (Ref Pattern)
// 当你既想保持函数引用不变,又想访问最新 state 时
const countRef = useRef(count);
useEffect(() => { countRef.current = count; }, [count]);
const handleLogAdvanced = useCallback(() => {
console.log('Current count:', countRef.current);
}, []); // 永远不更新引用,但能拿到最新值
useEffect(() => {
let ignore = false;
async function fetchProfile() {
const result = await api.fetchUser(userId);
// 如果组件卸载了,或者 userId 变了导致开启了新一轮 effect,
// 这里的 ignore 就会变成 true,从而丢弃旧的请求结果
if (!ignore) setProfile(result);
}
fetchProfile();
return () => { ignore = true; };
}, [userId]);
// 场景:实现 usePrevious Hook
function usePrevious(value) {
const ref = useRef();
// 每次渲染都会执行,但在 return 之前更新
// 所以本次渲染拿到的 ref.current 还是上次的值
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
// 只暴露 focus,不暴露其他原生 DOM 方法,更安全
}));
return <input ref={inputRef} />;
});
const deferredQuery = useDeferredValue(query);
// 列表只依赖 deferredQuery,当 query 快速变化时,deferredQuery 会滞后更新
// 从而减少列表重渲染次数
// 优化后的 Context 结构
export const UserStateContext = createContext<State | null>(null);
export const UserDispatchContext = createContext<Dispatch<Action> | null>(null);
export const UserProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<UserDispatchContext.Provider value={dispatch}>
<UserStateContext.Provider value={state}>
{children}
</UserStateContext.Provider>
</UserDispatchContext.Provider>
);
};
// 收益:
// 只需要触发更新的组件(比如一个按钮)可以只 consume UserDispatchContext。
// 当 state 变化时,这个按钮组件 **不会** 重渲染,因为它不依赖 UserStateContext。
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');
// 多余的渲染 pass:render -> useEffect -> setState -> re-render
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// 直接在渲染过程中计算。React 极快,不要担心这点计算量。
const fullName = `${firstName} ${lastName}`;
// 改造前
<Page user={user} /> // Page 内部要把 user 一层层传下去
// 改造后
<Page>
<Avatar user={user} /> {/* 直接在这里使用 user */}
</Page>