React 高级技巧:从熟练到精通
写给有一定 React 基础、想要突破瓶颈的前端开发者。本文不讲 useState 和 useEffect 的基础用法,而是聚焦那些能真正提升代码质量和应用性能的进阶模式。
一、用 useRef 做"不触发渲染的状态"
很多人只把 useRef 当作获取 DOM 节点的工具,但它真正的能力在于——持有一个跨渲染周期稳定的可变值,且修改它不会触发 re-render。
典型场景:在 useEffect 的回调或定时器里需要访问"最新的 props/state",但又不想把它加进依赖数组。
function useLatest(value) {
const ref = useRef(value);
ref.current = value; // 每次渲染都同步最新值
return ref;
}
function Chat({ onMessage }) {
const onMessageRef = useLatest(onMessage);
useEffect(() => {
const ws = new WebSocket('/chat');
ws.onmessage = (e) => onMessageRef.current(e.data);
return () => ws.close();
}, []); // 依赖数组为空,但回调永远是最新的
}
这个 useLatest 模式在社区中被广泛使用(ahooks、react-use 等库都内置了它),核心思路就是把"值的引用"和"副作用的生命周期"解耦。
二、组件拆分的"状态下沉"原则
性能优化最常见的误区是到处加 React.memo。更根本的思路是——把状态下沉到真正需要它的子树里,让无关组件压根不参与 re-render。
// ❌ 整个页面因为 hover 状态频繁 re-render
function Page() {
const [hovered, setHovered] = useState(false);
return (
<div>
<HeavyHeader />
<div onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ background: hovered ? '#f0f0f0' : '#fff' }}>
Hover me
</div>
<HeavyFooter />
</div>
);
}
// ✅ 把 hover 状态封装到独立组件
function HoverBox() {
const [hovered, setHovered] = useState(false);
return (
<div onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ background: hovered ? '#f0f0f0' : '#fff' }}>
Hover me
</div>
);
}
function Page() {
return (
<div>
<HeavyHeader />
<HoverBox />
<HeavyFooter />
</div>
);
}
这比 React.memo 更彻底——不是"渲染了再比较要不要跳过",而是从源头上缩小了渲染范围。
三、用 children 模式阻断 re-render 传播
与状态下沉相反的场景:状态必须在外层,但子组件不依赖这个状态。这时可以利用 children 的稳定性。
// ❌ ScrollY 变化导致 children 全部 re-render
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handler);
return () => window.removeEventListener('scroll', handler);
}, []);
return (
<div>
<Navbar transparent={scrollY < 100} />
<HeavyContent /> {/* 每次滚动都 re-render */}
</div>
);
}
// ✅ 把不依赖 scrollY 的部分通过 children 传入
function ScrollProvider({ children }) {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handler);
return () => window.removeEventListener('scroll', handler);
}, []);
return (
<div>
<Navbar transparent={scrollY < 100} />
{children} {/* children 的引用没变,不会 re-render */}
</div>
);
}
// 使用
<ScrollProvider>
<HeavyContent />
</ScrollProvider>
原理:children 是在父组件(ScrollProvider 的调用方)渲染时创建的 JSX 元素,ScrollProvider 内部状态变化不会改变 children 的引用。
四、useReducer + Context:轻量级状态管理
当多个组件需要共享一块状态时,不一定要引入 Redux 或 Zustand。useReducer + Context 可以覆盖大量场景,关键在于把 dispatch 和 state 拆成两个 Context。
const StateCtx = createContext();
const DispatchCtx = createContext();
function reducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return { ...state, todos: [...state.todos, action.payload] };
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((t, i) =>
i === action.index ? { ...t, done: !t.done } : t
),
};
default:
return state;
}
}
function TodoProvider({ children }) {
const [state, dispatch] = useReducer(reducer, { todos: [] });
return (
<DispatchCtx.Provider value={dispatch}>
<StateCtx.Provider value={state}>
{children}
</StateCtx.Provider>
</DispatchCtx.Provider>
);
}
为什么要拆?因为 dispatch 是稳定的引用(React 保证),而 state 每次变化都是新对象。如果只有一个 Context,那些只需要 dispatch(比如"添加按钮")的组件也会因为 state 变化而 re-render。拆开之后,只订阅 DispatchCtx 的组件完全不受 state 更新影响。
五、自定义 Hook 的组合模式
自定义 Hook 的真正威力不在于封装一个 useXxx,而在于多个 Hook 像乐高一样组合。
// 基础 Hook:监听媒体查询
function useMediaQuery(query) {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const mql = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
}, [query]);
return matches;
}
// 基础 Hook:本地存储
function useLocalStorage(key, initial) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// 组合:响应式暗色模式
function useDarkMode() {
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
const [override, setOverride] = useLocalStorage('theme', null);
const isDark = override !== null ? override === 'dark' : prefersDark;
const toggle = () => setOverride(isDark ? 'light' : 'dark');
const reset = () => setOverride(null); // 回到跟随系统
return { isDark, toggle, reset };
}
每个基础 Hook 只做一件事,组合 Hook 负责编排逻辑。测试和复用都变得很容易。
六、用 useSyncExternalStore 接管外部数据源
React 18 引入的 useSyncExternalStore 是订阅外部数据源的"正统"方式,解决了 useEffect + setState 模式在并发渲染下可能出现的"撕裂"问题。
import { useSyncExternalStore } from 'react';
// 封装一个极简的 store
function createStore(initialState) {
let state = initialState;
const listeners = new Set();
return {
getState: () => state,
setState: (fn) => {
state = typeof fn === 'function' ? fn(state) : fn;
listeners.forEach((l) => l());
},
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
const counterStore = createStore({ count: 0 });
function useStore(store, selector) {
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState())
);
}
// 组件中使用
function Counter() {
const count = useStore(counterStore, (s) => s.count);
return (
<button onClick={() => counterStore.setState((s) => ({ count: s.count + 1 }))}>
{count}
</button>
);
}
这个模式就是 Zustand 的核心原理。理解了它,你就理解了为什么 Zustand 那么轻量,以及它在并发模式下为什么比手写 useEffect 订阅更可靠。
七、Compound Components 复合组件模式
当你在构建一个 UI 组件库时,复合组件模式可以提供极其灵活的 API 设计。
const TabsContext = createContext();
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ children, index }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
return (
<button
role="tab"
className={activeIndex === index ? 'active' : ''}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
}
function TabPanels({ children }) {
const { activeIndex } = useContext(TabsContext);
return <div className="tab-panels">{Children.toArray(children)[activeIndex]}</div>;
}
function TabPanel({ children }) {
return <div role="tabpanel">{children}</div>;
}
// 挂载子组件
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;
使用时像这样:
<Tabs defaultIndex={0}>
<Tabs.List>
<Tabs.Tab index={0}>详情</Tabs.Tab>
<Tabs.Tab index={1}>评论</Tabs.Tab>
<Tabs.Tab index={2}>相关</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>详情内容...</Tabs.Panel>
<Tabs.Panel>评论内容...</Tabs.Panel>
<Tabs.Panel>相关内容...</Tabs.Panel>
</Tabs.Panels>
</Tabs>
这种模式的优势是:结构完全由使用者控制,组件之间通过 Context 隐式通信,既灵活又保持了内聚性。Radix UI、Headless UI 等库都大量使用了这个模式。
八、React.lazy + Suspense 的实战细节
代码分割大家都知道用 React.lazy,但有几个细节容易踩坑。
1. 把 lazy 声明放在模块顶层,不要放在组件里:
// ✅ 模块顶层,只执行一次
const Editor = lazy(() => import('./Editor'));
// ❌ 组件内部,每次渲染都创建新的 lazy 组件
function Page() {
const Editor = lazy(() => import('./Editor'));
// ...
}
2. 用工厂函数做预加载:
const importEditor = () => import('./Editor');
const Editor = lazy(importEditor);
// 鼠标移入时预加载,而不是等点击
<button onMouseEnter={importEditor} onClick={() => setShowEditor(true)}>
打开编辑器
</button>
3. 嵌套 Suspense 做细粒度加载态:
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</Suspense>
外层 Suspense 兜底整个页面,内层 Suspense 让各区域独立加载,避免一个慢接口阻塞整个页面。
九、ErrorBoundary 的现代写法
类组件的 ErrorBoundary 是 React 唯一还需要类组件的地方。但我们可以用一个通用的封装让它在函数组件中好用起来。
class ErrorBoundary extends Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, info) {
// 上报错误监控
reportError(error, info.componentStack);
}
render() {
if (this.state.error) {
return this.props.fallback(this.state.error, () =>
this.setState({ error: null })
);
}
return this.props.children;
}
}
// 使用
<ErrorBoundary
fallback={(error, retry) => (
<div>
<p>出了点问题:{error.message}</p>
<button onClick={retry}>重试</button>
</div>
)}
>
<App />
</ErrorBoundary>
把 retry(重置错误状态)作为参数传给 fallback,用户点击"重试"后子树会重新挂载,这在网络请求失败的场景下非常实用。
十、性能排查的正确姿势
最后聊聊性能排查。与其凭直觉优化,不如用工具定位问题。
React DevTools Profiler 是第一步。录制一段交互,找到耗时最长的提交(commit),展开看哪些组件在 re-render、每次渲染耗时多少。
why-did-you-render 库可以在开发环境自动检测不必要的 re-render,帮你发现那些"props 看起来没变但引用变了"的隐蔽问题。
一个常见的"引用陷阱":
// ❌ 每次渲染都创建新的 style 对象
<div style={{ color: 'red' }}>
// ✅ 提到模块顶层或用 useMemo
const redText = { color: 'red' };
<div style={redText}>
同样的问题也出现在内联函数、内联数组上。解决方案要么提取为常量,要么用 useMemo / useCallback——但只在确认存在性能问题时才用,不要过早优化。
写在最后
React 的进阶之路不在于记住更多 API,而在于理解它的渲染模型和数据流。上面这些技巧的底层逻辑其实只有两条:
- 控制渲染范围——让该更新的更新,不该更新的别动。
- 保持引用稳定——React 靠引用比较决定是否 re-render,管理好引用就管好了性能。
把这两条内化,遇到具体问题时自然能推导出解法,而不需要死记硬背每一个模式。