最近React团队发布了Hooks的alpha预览版引起了前端社区的广泛关注。

Hooks能让函数组件拥有状态和生命周期等类组件的特性不用再写class代码更简洁更优雅也能更好地复用状态逻辑。

我通过特殊渠道提前体验了Hooks的内部预览版在实际项目中用了一段时间踩了很多坑也积累了一些实战经验。

注意Hooks目前还是alpha预览版API可能会变化本文的内容基于当前的预览版仅供参考正式版发布后可能会有调整。

今天想总结一下React Hooks的踩坑经验和,实战技巧帮大家更好地理解和使用Hooks。

一、Hooks简介

在讲踩坑之前,先简单介绍一下Hooks。

Hooks是React 16.7(预览版)引入的新特性它能让你在函数组件中使用状态和,其他React特性不用写class。

Hooks的核心思想是让函数组件也能拥有类组件的能力,同时避免class的一些问题,比如this指向混乱生命周期方法里逻辑分散状态逻辑难以复用等等。

常用的Hooks

  1. useState:在函数组件中添加状态。
  2. useEffect:在函数组件中处理副作用类似类组件的生命周期方法。
  3. useContext:在函数组件中使用Context。
  4. useReducer:在函数组件中使用reducer管理复杂状态。
  5. useCallback:缓存回调函数避免不必要的重新渲染。
  6. useMemo:缓存计算结果避免重复计算。
  7. useRef:在函数组件中使用ref。
  8. useImperativeMethods:自定义暴露给父组件的实例值。
  9. useLayoutEffect:在DOM更新后同步执行副作用。

Hooks的出现是React的一个重大变革能让React代码更简洁更优雅也能更好地复用逻辑。

二、踩坑1:useState的初始值

useState是最常用的Hook用来在函数组件中添加状态。

const [count, setCount] = useState(0);

坑:初始值只在第一次渲染时生效

很多人刚开始用useState的时候,会以为每次渲染都会用传入的初始值重置状态其实不是useState的初始值只在第一次渲染时生效后续渲染会忽略初始值用当前的状态。

比如:

function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  // ...
}

如果父组件传入的initialCount变化了子组件的count不会跟着变化,因为useState的初始值只在第一次渲染时用后续渲染忽略。

如果需要根据props变化更新状态要用useEffect监听props变化手动更新状态:

function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  
  useEffect(() => {
    setCount(initialCount);
  }, [initialCount]);
  
  // ...
}

或者用key强制重新挂载组件让useState重新初始化:

<Counter key={initialCount} initialCount={initialCount} />

坑:函数式更新

如果新的状态依赖旧的状态要用函数式更新不要直接用旧的状态,因为闭包可能会捕获旧的状态值。

// 不好的写法可能捕获旧的count
setCount(count + 1);

// 好的写法函数式更新保证拿到最新的count
setCount(prevCount => prevCount + 1);

特别是在setTimeoutsetInterval或者事件回调里更新状态一定要用函数式更新,否则可能会用到旧的状态导致bug。

三、踩坑2:useEffect的依赖数组

useEffect是另一个常用的Hook用来处理副作用,比如数据获取订阅DOM操作等等。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]);

坑:依赖数组不正确

useEffect的第二个参数是依赖数组指定这个effect依赖哪些变量,只有这些变量变化时effect才会重新执行。

很多人刚开始用useEffect的时候,会忘记添加依赖,或者依赖添加不正确导致effect不执行,或者频繁执行,或者用到旧的变量值。

比如:

// 不好的写法没有依赖数组每次渲染都执行
useEffect(() => {
  fetchData();
});

// 不好的写法空依赖数组只在第一次渲染执行,但是用到了count会有闭包问题
useEffect(() => {
  console.log(count);
}, []);

// 好的写法正确添加依赖
useEffect(() => {
  console.log(count);
}, [count]);

坑:依赖数组里的函数

如果effect里用到了函数要注意函数的引用变化会导致effect重新执行。如果函数是在组件内部定义的每次渲染都会创建新的函数引用导致effect每次渲染都执行。

比如:

function MyComponent() {
  const fetchData = () => {
    // ...
  };
  
  useEffect(() => {
    fetchData();
  }, [fetchData]); // fetchData每次渲染都是,新的引用effect每次都执行
}

解决方法是用useCallback缓存函数,或者把函数移到组件外部,或者用useRef保存函数。

function MyComponent() {
  const fetchData = useCallback(() => {
    // ...
  }, []); // 用useCallback缓存依赖不变的话,引用不变
  
  useEffect(() => {
    fetchData();
  }, [fetchData]); // 现在,只有fetchData变化时才执行
}

坑:清理函数

useEffect可以返回一个清理函数在组件卸载,或者effect重新执行前调用用来清理副作用,比如取消订阅清除定时器等等。

很多人会忘记写清理函数导致内存泄漏,或者bug。

比如:

// 不好的写法没有清理定时器组件卸载后定时器还在跑
useEffect(() => {
  const timer = setInterval(() => {
    console.log('tick');
  }, 1000);
}, []);

// 好的写法返回清理函数清除定时器
useEffect(() => {
  const timer = setInterval(() => {
    console.log('tick');
  }, 1000);
  return () => clearInterval(timer); // 清理函数
}, []);

四、踩坑3:Hooks的调用规则

Hooks有严格的调用规则不遵守的话,会出问题。

规则1:只在顶层调用Hooks

不要在循环条件嵌套函数里调用Hooks要只在React函数组件的顶层调用。

// 不好的写法在条件里调用Hook
if (condition) {
  const [count, setCount] = useState(0);
}

// 不好的写法在循环里调用Hook
for (let i = 0; i < 10; i++) {
  const [value, setValue] = useState(i);
}

// 好的写法在顶层调用Hook
const [count, setCount] = useState(0);
if (condition) {
  // 用count
}

原因是React靠Hooks的调用顺序来对应状态和effect如果在,条件或循环里调用渲染顺序变化会导致Hooks对应错误出现bug。

规则2:只在React函数组件或自定义Hook里调用Hooks

不要在普通JavaScript函数里调用Hooks要只在React函数组件,或者自定义Hook里调用。

// 不好的写法在普通函数里调用Hook
function doSomething() {
  const [count, setCount] = useState(0);
}

// 好的写法在函数组件里调用Hook
function MyComponent() {
  const [count, setCount] = useState(0);
  // ...
}

// 好的写法在自定义Hook里调用Hook
function useCustomHook() {
  const [count, setCount] = useState(0);
  return count;
}

规则3:用ESLint插件检查

React团队提供了ESLint插件eslint-plugin-react-hooks能自动检查Hooks的调用规则避免犯错建议一定要安装和配置。

npm install eslint-plugin-react-hooks --save-dev
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

五、踩坑4:自定义Hook的命名和使用

自定义Hook是Hooks的一大亮点能把组件逻辑抽取出来复用。

坑:自定义Hook要以use开头

自定义Hook的函数名要以use开头,比如useFetchuseLocalStorage等等这样React和ESLint才能识别这是一个Hook检查调用规则。

// 不好的写法不以use开头
function fetchData(url) {
  const [data, setData] = useState(null);
  // ...
}

// 好的写法以use开头
function useFetch(url) {
  const [data, setData] = useState(null);
  // ...
}

坑:自定义Hook里的状态是独立的

每个组件调用自定义Hook得到的状态都是独立的不会共享状态。如果需要共享状态要用Context或者其他状态管理方案。

比如:

function useCounter() {
  const [count, setCount] = useState(0);
  return [count, setCount];
}

function ComponentA() {
  const [count, setCount] = useCounter(); // 独立的状态
  // ...
}

function ComponentB() {
  const [count, setCount] = useCounter(); // 另一个独立的状态和ComponentA不共享
  // ...
}

如果需要共享状态要用Context:

const CounterContext = createContext();

function CounterProvider({ children }) {
  const [count, setCount] = useState(0);
  return (
    <CounterContext.Provider value={[count, setCount]}>
      {children}
    </CounterContext.Provider>
  );
}

function useCounter() {
  return useContext(CounterContext);
}

坑:自定义Hook的依赖

自定义Hook如果接收参数要注意参数变化时Hook里的effect要重新执行要正确添加依赖。

比如:

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, [url]); // url变化时重新请求
  
  return [data, loading];
}

六、踩坑5:useReducer的使用

useReducer用来管理复杂的状态类似Redux的reducer。

坑:action类型要唯一

用useReducer的时候action的type要唯一不要重复,否则会导致状态更新错误。

坑:reducer要是纯函数

reducer必须是纯函数不要在reducer里做副作用,比如API请求定时器等等副作用要在useEffect里做。

坑:初始状态的计算

如果初始状态需要复杂计算可以给useReducer传第三个参数init函数惰性初始化避免每次渲染都计算。

function init(initialCount) {
  return { count: initialCount };
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter({ initialCount }) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  // ...
}

七、踩坑6:useCallback和useMemo

useCallback和useMemo用来缓存函数和计算结果优化性能。

坑:不要滥用

很多人刚开始用Hooks的时候,会把所有函数都包上useCallback所有计算都包上useMemo以为这样性能更好其实,不然。

useCallback和useMemo本身也有开销需要保存缓存比较依赖等等,如果函数或计算很简单开销可能比重新创建还大反而影响性能。

只有当函数传给子组件会导致子组件重新渲染,或者计算很昂贵的时候,才需要用useCallback或useMemo。

坑:依赖数组要正确

useCallback和useMemo的依赖数组也要正确,否则会用到旧的变量值导致bug。

// 不好的写法依赖不正确用到旧的count
const handleClick = useCallback(() => {
  console.log(count);
}, []);

// 好的写法正确添加依赖
const handleClick = useCallback(() => {
  console.log(count);
}, [count]);

八、踩坑7:useRef的使用

useRef用来在函数组件中使用ref也能保存任何可变值类似实例变量。

坑:ref变化不会触发重新渲染

useRef返回的ref对象的current属性变化不会触发组件重新渲染,如果需要根据值变化重新渲染要用useState不要用useRef。

坑:ref.current的初始值

useRef的初始值只在第一次渲染时生效后续渲染忽略和useState类似。

坑:用useRef保存定时器ID

在函数组件里用定时器的时候,可以用useRef保存定时器ID方便清理。

function Timer() {
  const timerRef = useRef(null);
  
  useEffect(() => {
    timerRef.current = setInterval(() => {
      console.log('tick');
    }, 1000);
    return () => clearInterval(timerRef.current);
  }, []);
  
  // ...
}

九、实战经验

除了上面的踩坑我还积累了一些实战经验分享给大家。

经验1:先在小组件里试用

Hooks目前还是alpha预览版不要一上来就在大型项目里全面使用先在,小的组件里试用熟悉API和坑再逐步推广。

经验2:和类组件共存

Hooks和类组件可以共存不用一次性把所有类组件都改成函数组件可以逐步迁移新的组件用Hooks旧的组件保持类组件慢慢改。

经验3:自定义Hook复用逻辑

Hooks最大的好处是能更好地复用状态逻辑以前用HOC或者render props复用逻辑很麻烦现在用自定义Hook很方便。

建议把通用的状态逻辑抽取成自定义Hook比如useFetchuseLocalStorageuseDebounceuseWindowSize等等复用提高开发效率。

经验4:注意性能优化

函数组件配合Hooks要注意性能优化,比如用React.memo避免不必要的重新渲染用useCallback缓存回调用useMemo缓存计算结果等等。

但是也不要过度优化先保证代码正确和可读再考虑性能优化性能瓶颈的地方再优化。

经验5:测试

Hooks的测试和类组件不太一样要用专门的测试工具,比如react-hooks-testing-library能方便地测试自定义Hook。

建议给自定义Hook写单元测试保证逻辑正确。

十、写在最后

以上就是我使用React Hooks的踩坑总结和实战经验。

Hooks是React的一个重大变革能让React代码更简洁更优雅也能更好地复用逻辑是React的未来方向。

但是Hooks目前还是alpha预览版API可能会变化也有一些坑需要注意不要在,生产环境的核心项目里贸然使用先在小项目或非核心组件里试用熟悉了再推广。

希望我的踩坑经验能帮大家更好地理解和使用Hooks少踩坑多办事。

如果有什么问题,或者更好的经验欢迎在评论区留言我们一起交流。

最后用一句话结束这篇文章:"Hooks是React的未来早学早受益,但是也要注意踩坑。"

愿大家都能用好React Hooks写出更优雅的React代码。