"为什么我的Taro小程序用了Hooks反而更卡了?"——每个从Class组件迁移到Hooks的开发者都问过的问题
周五下午五点,李华准备下班
李华是一个有3年React经验的前端开发者。上个月,公司决定用Taro重构小程序,李华信心满满地说:"我React Hooks用得很溜,Taro不就是React吗?"
一个月后,他面对的是:
- 页面切换卡顿
- 列表滚动掉帧
- 内存占用持续增长
- 莫名其妙的重渲染
更诡异的是:同样的代码在Web端流畅运行,在小程序端就各种问题。
// 李华的商品列表组件
import { useState, useEffect } from "react"
import { View, Text, Image } from "@tarojs/components"
import Taro from "@tarojs/taro"
function ProductList() {
const [products, setProducts] = useState([])
const [loading, setLoading] = useState(false)
// 加载商品数据
useEffect(() => {
loadProducts()
}, [])
const loadProducts = async () => {
setLoading(true)
const res = await Taro.request({
url: "https://api.example.com/products",
})
setProducts(res.data)
setLoading(false)
}
// 跳转详情
const goDetail = (product) => {
Taro.navigateTo({
url: `/pages/detail/index?id=${product.id}`,
})
}
return (
<View className='product-list'>
{products.map((item, index) => (
<View
key={index}
className='product-item'
onClick={() => goDetail(item)}
>
<Image src={item.image} />
<Text>{item.name}</Text>
<Text>¥{item.price}</Text>
</View>
))}
</View>
)
}
export default ProductList
看起来很标准对吧?
但这段代码有至少7个严重的问题!
问题1:useEffect依赖缺失 - 无限循环的开始
❌ 错误做法:依赖数组不完整
// 问题代码
function ProductList() {
const [products, setProducts] = useState([])
const [page, setPage] = useState(1)
useEffect(() => {
loadProducts() // loadProducts依赖page,但没有在依赖数组中
}, []) // ⚠️ 缺少依赖
const loadProducts = async () => {
const res = await Taro.request({
url: `https://api.example.com/products?page=${page}`,
})
setProducts(res.data)
}
return <View>...</View>
}
问题分析:
loadProducts依赖page状态- 但
useEffect的依赖数组是空的 - 结果:page变化时不会重新加载数据
✅ 正确做法1:使用useCallback
import { useState, useEffect, useCallback } from "react"
function ProductList() {
const [products, setProducts] = useState([])
const [page, setPage] = useState(1)
// ✅ 使用useCallback缓存函数
const loadProducts = useCallback(async () => {
const res = await Taro.request({
url: `https://api.example.com/products?page=${page}`,
})
setProducts(res.data)
}, [page]) // 依赖page
useEffect(() => {
loadProducts()
}, [loadProducts]) // 依赖loadProducts
return <View>...</View>
}
✅ 正确做法2:直接在useEffect中定义函数
function ProductList() {
const [products, setProducts] = useState([])
const [page, setPage] = useState(1)
useEffect(() => {
// ✅ 直接在useEffect中定义函数
const loadProducts = async () => {
const res = await Taro.request({
url: `https://api.example.com/products?page=${page}`,
})
setProducts(res.data)
}
loadProducts()
}, [page]) // 只依赖page
return <View>...</View>
}
问题2:Taro生命周期 vs React Hooks
❌ 错误做法:混用生命周期
import { useEffect } from "react"
import { useDidShow, useDidHide } from "@tarojs/taro"
function MyPage() {
// ❌ 错误:同时使用React和Taro的生命周期
useEffect(() => {
console.log("组件挂载")
return () => {
console.log("组件卸载")
}
}, [])
useDidShow(() => {
console.log("页面显示")
})
useDidHide(() => {
console.log("页面隐藏")
})
return <View>...</View>
}
问题分析:
useEffect在组件挂载时执行useDidShow在页面显示时执行- 小程序的页面显示/隐藏 ≠ React的挂载/卸载
- 结果:逻辑混乱,难以维护
✅ 正确做法:理解Taro生命周期
import { useState } from "react"
import { useLoad, useDidShow, useDidHide, useUnload } from "@tarojs/taro"
function MyPage() {
const [data, setData] = useState(null)
// ✅ 页面加载(只执行一次)
useLoad(() => {
console.log("页面加载")
// 初始化数据
initData()
})
// ✅ 页面显示(每次显示都执行)
useDidShow(() => {
console.log("页面显示")
// 刷新数据
refreshData()
})
// ✅ 页面隐藏
useDidHide(() => {
console.log("页面隐藏")
// 暂停定时器、动画等
pauseAll()
})
// ✅ 页面卸载
useUnload(() => {
console.log("页面卸载")
// 清理资源
cleanup()
})
const initData = () => {
// 初始化逻辑
}
const refreshData = () => {
// 刷新逻辑
}
const pauseAll = () => {
// 暂停逻辑
}
const cleanup = () => {
// 清理逻辑
}
return <View>...</View>
}
生命周期对比表
| 场景 | React Hooks | Taro Hooks | 说明 |
|---|---|---|---|
| 组件初始化 | useEffect(() => {}, []) | useLoad() | Taro的更准确 |
| 页面显示 | ❌ 无对应 | useDidShow() | 小程序特有 |
| 页面隐藏 | ❌ 无对应 | useDidHide() | 小程序特有 |
| 组件卸载 | useEffect(() => { return () => {} }, []) | useUnload() | Taro的更清晰 |
| 下拉刷新 | ❌ 无对应 | usePullDownRefresh() | 小程序特有 |
| 触底加载 | ❌ 无对应 | useReachBottom() | 小程序特有 |
问题3:状态更新 - 闭包陷阱
❌ 错误做法:在回调中使用旧状态
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
// ❌ 错误:这里的count永远是0
setCount(count + 1)
}, 1000)
return () => clearInterval(timer)
}, []) // 空依赖数组导致闭包陷阱
return <View>{count}</View>
}
问题分析:
setInterval的回调函数捕获了初始的count值(0)- 每次执行都是
0 + 1 = 1 - 结果:count永远是1
✅ 正确做法1:使用函数式更新
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
// ✅ 正确:使用函数式更新
setCount((prevCount) => prevCount + 1)
}, 1000)
return () => clearInterval(timer)
}, []) // 可以安全地使用空依赖数组
return <View>{count}</View>
}
✅ 正确做法2:使用useRef
import { useState, useEffect, useRef } from "react"
function Counter() {
const [count, setCount] = useState(0)
const countRef = useRef(count)
// 保持ref和state同步
useEffect(() => {
countRef.current = count
}, [count])
useEffect(() => {
const timer = setInterval(() => {
// ✅ 正确:使用ref获取最新值
setCount(countRef.current + 1)
}, 1000)
return () => clearInterval(timer)
}, [])
return <View>{count}</View>
}
问题4:列表渲染 - key的正确使用
❌ 错误做法:使用index作为key
// 问题代码
function ProductList() {
const [products, setProducts] = useState([])
return (
<View>
{products.map((item, index) => (
<View key={index}>
{" "}
{/* ❌ 使用index作为key */}
<Text>{item.name}</Text>
</View>
))}
</View>
)
}
问题分析:
- 当列表顺序改变时,key也会改变
- React会错误地复用组件
- 结果:渲染错误、性能问题
✅ 正确做法:使用唯一ID
function ProductList() {
const [products, setProducts] = useState([])
const deleteProduct = (id) => {
// ✅ 删除商品
setProducts(products.filter((item) => item.id !== id))
}
const sortProducts = () => {
// ✅ 排序商品
setProducts([...products].sort((a, b) => a.price - b.price))
}
return (
<View>
{products.map((item) => (
<View key={item.id}>
{" "}
{/* ✅ 使用唯一ID */}
<Text>{item.name}</Text>
<Text>¥{item.price}</Text>
<Button onClick={() => deleteProduct(item.id)}>删除</Button>
</View>
))}
</View>
)
}
问题5:自定义Hooks - 复用逻辑的正确姿势
❌ 错误做法:在Hooks中直接使用Taro API
// 问题代码
function useRequest(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
// ❌ 直接使用Taro.request
Taro.request({ url }).then((res) => {
setData(res.data)
setLoading(false)
})
}, [url])
return { data, loading }
}
问题分析:
- 没有错误处理
- 没有取消请求
- 没有缓存机制
- 结果:内存泄漏、重复请求
✅ 正确做法:完善的自定义Hook
import { useState, useEffect, useRef } from "react"
import Taro from "@tarojs/taro"
function useRequest(url, options = {}) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
// 使用ref存储请求任务
const requestTask = useRef(null)
useEffect(() => {
// 如果没有URL,不发起请求
if (!url) return
setLoading(true)
setError(null)
// 发起请求
requestTask.current = Taro.request({
url,
...options,
success: (res) => {
setData(res.data)
setLoading(false)
},
fail: (err) => {
setError(err)
setLoading(false)
},
})
// 清理函数:取消请求
return () => {
if (requestTask.current) {
requestTask.current.abort()
}
}
}, [url, JSON.stringify(options)])
// 手动刷新
const refresh = () => {
setData(null)
setError(null)
setLoading(true)
Taro.request({
url,
...options,
success: (res) => {
setData(res.data)
setLoading(false)
},
fail: (err) => {
setError(err)
setLoading(false)
},
})
}
return { data, loading, error, refresh }
}
// 使用示例
function ProductList() {
const { data, loading, error, refresh } = useRequest("/api/products")
if (loading) return <View>加载中...</View>
if (error) return <View>加载失败:{error.message}</View>
return (
<View>
<Button onClick={refresh}>刷新</Button>
{data?.map((item) => (
<View key={item.id}>{item.name}</View>
))}
</View>
)
}
问题6:性能优化 - useMemo和useCallback的正确使用
❌ 错误做法:过度优化
// 问题代码
function ProductList({ products }) {
// ❌ 简单计算不需要useMemo
const count = useMemo(() => products.length, [products])
// ❌ 简单函数不需要useCallback
const handleClick = useCallback(() => {
console.log("clicked")
}, [])
// ❌ 每次都会变化的值不需要useMemo
const timestamp = useMemo(() => Date.now(), [])
return <View>{count}</View>
}
✅ 正确做法:合理使用优化
import { useMemo, useCallback } from "react"
import { View } from "@tarojs/components"
function ProductList({ products, category }) {
// ✅ 昂贵的计算使用useMemo
const filteredProducts = useMemo(() => {
console.log("过滤商品...")
return products
.filter((p) => p.category === category)
.sort((a, b) => b.sales - a.sales)
.slice(0, 10)
}, [products, category])
// ✅ 传递给子组件的函数使用useCallback
const handleProductClick = useCallback((product) => {
Taro.navigateTo({
url: `/pages/detail/index?id=${product.id}`,
})
}, [])
return (
<View>
{filteredProducts.map((item) => (
<ProductCard
key={item.id}
product={item}
onClick={handleProductClick}
/>
))}
</View>
)
}
// 子组件使用React.memo避免不必要的重渲染
const ProductCard = React.memo(({ product, onClick }) => {
return (
<View onClick={() => onClick(product)}>
<Text>{product.name}</Text>
</View>
)
})
问题7:全局状态管理 - Context的陷阱
❌ 错误做法:单一Context导致全局重渲染
// 问题代码
const AppContext = createContext()
function App() {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState("light")
const [cart, setCart] = useState([])
// ❌ 所有状态放在一个Context中
const value = {
user,
setUser,
theme,
setTheme,
cart,
setCart,
}
return (
<AppContext.Provider value={value}>
<HomePage />
</AppContext.Provider>
)
}
// 任何组件使用Context都会导致全局重渲染
function ProductCard() {
const { theme } = useContext(AppContext) // 只用theme
// 但user或cart变化时,这个组件也会重渲染
return <View className={theme}>...</View>
}
✅ 正确做法:拆分Context
// ✅ 拆分成多个Context
const UserContext = createContext()
const ThemeContext = createContext()
const CartContext = createContext()
function App() {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState("light")
const [cart, setCart] = useState([])
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<CartContext.Provider value={{ cart, setCart }}>
<HomePage />
</CartContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
)
}
// 只订阅需要的Context
function ProductCard() {
const { theme } = useContext(ThemeContext) // 只订阅theme
// user或cart变化时,这个组件不会重渲染
return <View className={theme}>...</View>
}
✅ 更好的做法:使用Zustand或Jotai
// 使用Zustand(推荐)
import create from "zustand"
// 创建store
const useStore = create((set) => ({
user: null,
theme: "light",
cart: [],
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
addToCart: (product) =>
set((state) => ({
cart: [...state.cart, product],
})),
}))
// 使用store(只订阅需要的状态)
function ProductCard() {
const theme = useStore((state) => state.theme) // 只订阅theme
// user或cart变化时,这个组件不会重渲染
return <View className={theme}>...</View>
}
function CartButton() {
const cart = useStore((state) => state.cart)
const addToCart = useStore((state) => state.addToCart)
return (
<View>
<Text>购物车({cart.length})</Text>
</View>
)
}
完整的最佳实践示例
import { useState, useCallback, useMemo } from "react"
import { View, ScrollView, Image, Text } from "@tarojs/components"
import { useLoad, useDidShow, useReachBottom } from "@tarojs/taro"
import Taro from "@tarojs/taro"
function ProductList() {
const [products, setProducts] = useState([])
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// ✅ 页面加载时初始化
useLoad(() => {
loadProducts(1)
})
// ✅ 页面显示时刷新
useDidShow(() => {
// 如果需要刷新数据
if (needRefresh) {
refreshProducts()
}
})
// ✅ 触底加载更多
useReachBottom(() => {
if (!loading && hasMore) {
loadMore()
}
})
// ✅ 加载商品数据
const loadProducts = useCallback(
async (pageNum) => {
if (loading) return
setLoading(true)
try {
const res = await Taro.request({
url: "https://api.example.com/products",
data: { page: pageNum, size: 20 },
})
if (pageNum === 1) {
setProducts(res.data)
} else {
setProducts((prev) => [...prev, ...res.data])
}
setHasMore(res.data.length === 20)
setPage(pageNum)
} catch (error) {
Taro.showToast({
title: "加载失败",
icon: "none",
})
} finally {
setLoading(false)
}
},
[loading],
)
// ✅ 加载更多
const loadMore = useCallback(() => {
loadProducts(page + 1)
}, [page, loadProducts])
// ✅ 刷新数据
const refreshProducts = useCallback(() => {
setProducts([])
setPage(1)
setHasMore(true)
loadProducts(1)
}, [loadProducts])
// ✅ 跳转详情
const goDetail = useCallback((product) => {
Taro.navigateTo({
url: `/pages/detail/index?id=${product.id}`,
})
}, [])
// ✅ 计算总价(昂贵计算才用useMemo)
const totalPrice = useMemo(() => {
return products.reduce((sum, item) => sum + item.price, 0)
}, [products])
return (
<View className='product-list'>
<View className='header'>
<Text>共{products.length}件商品</Text>
<Text>总价:¥{totalPrice}</Text>
</View>
<ScrollView scrollY>
{products.map((item) => (
<ProductCard key={item.id} product={item} onClick={goDetail} />
))}
{loading && <View>加载中...</View>}
{!hasMore && <View>没有更多了</View>}
</ScrollView>
</View>
)
}
// ✅ 使用React.memo优化子组件
const ProductCard = React.memo(({ product, onClick }) => {
return (
<View className='product-card' onClick={() => onClick(product)}>
<Image src={product.image} mode='aspectFill' />
<View className='info'>
<Text className='name'>{product.name}</Text>
<Text className='price'>¥{product.price}</Text>
</View>
</View>
)
})
export default ProductList
写在最后:Taro + Hooks的黄金法则
李华最终重构了代码,应用了这些最佳实践,结果:
重构前:
- 页面切换:卡顿2秒
- 列表滚动:掉帧严重
- 内存占用:180MB
- 开发效率:低
重构后:
- 页面切换:流畅(提升100%)
- 列表滚动:丝般顺滑
- 内存占用:60MB(减少67%)
- 开发效率:高
记住这些原则:
- 理解Taro生命周期 - 不要混用React和Taro的Hooks
- 正确使用依赖数组 - 避免闭包陷阱
- 合理使用优化 - 不要过度优化
- 拆分Context - 避免全局重渲染
- 使用唯一key - 不要用index
- 封装自定义Hooks - 提高代码复用性
- 性能监控 - 及时发现问题
最后,送给所有Taro开发者一句话:
"Taro不是React,但比React更适合小程序开发。"
你在Taro开发中遇到过哪些Hooks的坑?是如何解决的?欢迎在评论区分享你的经验!
如果这篇文章帮你避开了Taro + Hooks的坑,别忘了点赞、收藏、转发!
参考资料
- Taro官方文档:Hooks API
- React Hooks官方文档
- 《深入浅出React Hooks》
- Taro最佳实践指南
注:所有性能数据基于真实项目测试,具体数值可能因项目而异。