与类组件不同,React Hooks提供了底层API,以最少的样板即可优化和组合应用程序。
如果没有深入理解,那么就会因为一些细微的错误和资源泄漏,出现性能问题,而且代码复杂性也会增加。
我创建了13个示例,以演示常见问题以及解决方法。我还编写了React Hooks 雷达和React Hooks 建议清单,以提供一些小的建议和快速参考。
案例研究:实现定时器
目标是实现从0开始并每500ms增加的计数器。应该提供三个控制按钮:开始,停止和清除。

1. Hello World
export default function Demo1() {
console.log('renderDemo1');
const [count, setCount] = useState(0);
return (
<div>
count => {count}
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</div>
);
}
这是一个简单且正确实现的计数器,在用户点击时会增加或减少。
2. setInterval
export default function Demo2() {
console.log('renderDemo2');
const [count, setCount] = useState(0);
setInterval(() => {
setCount(count + 1);
}, 500);
return <div>count => {count}</div>;
}
该代码的目的是每隔500ms,计数加1。该代码存在大量资源泄漏,并且执行不正确。它将很容易造成浏览器选项卡崩溃。由于每次渲染时都会调用Demo2函数,因此每次触发渲染时,此组件都会创建新的interval。
在功能组件的主体内(称为React的渲染阶段),不允许进行突变,订阅,计时器,日志记录和其他副作用。这样做会导致UI中的错误和不一致。
Hooks API参考:useEffect
3. useEffect
export default function Demo3() {
console.log('renderDemo3');
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 500);
});
return <div>Level 2: count => {count}</div>;
}
大多数副作用发生在useEffect内。此代码还存在大量资源泄漏,并且实现不正确。 useEffect的默认行为是在每次渲染后运行,因此每次计数更改时都会创建新的interval。
4. run only once只执行一次
export default function Demo4() {
console.log('renderDemo4');
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 300);
}, []);
return <div>count => {count}</div>;
}
将[]作为useEffect的第二个参数将在组件加载后调用一次函数。即使setInterval仅被调用一次,该代码也不正确。
计数将从0增加到1并保持不变。箭头函数将创建一次,当这种情况发生时,计数将为0。
该代码具有轻微的资源泄漏。即使在卸载组件后,仍将调用setCount。
5. cleanup清理
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 300);
return () => clearInterval(interval);
}, []);
为了防止资源泄漏,必须在钩子的生命周期结束时处理所有东西。在这种情况下,将在组件卸载后调用返回的函数。
该代码没有资源泄漏,但是与上一个代码一样,实现不正确。
6. use count
as dependency
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 500);
return () => clearInterval(interval);
}, [count]);
将依赖项数组赋予useEffect将更改其生命周期。在此示例中,useEffect将在组件加载后调用一次,并且每次计数更改时都会调用一次。每当计数更改,处理之前的资源时,都会调用清除功能。
这段代码可以正常运行,没有任何错误,但是会引起误解。每500毫秒创建并处理一次setInterval。每个setInterval始终被调用一次。
7. setTimeout
useEffect(() => {
const timeout = setTimeout(() => {
setCount(count + 1);
}, 500);
return () => clearTimeout(timeout);
}, [count]);
此代码和上面的代码都能正常工作。由于useEffect在每次计数更改时都被调用,因此使用setTimeout与调用setInterval具有相同的效果。
此示例效率很低,每次渲染发生时都会创建新的setTimeout。 React有更好的方法来解决问题。
8. functional updates for useState
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1);
}, 500);
return () => clearInterval(interval);
}, []);
在前面的示例中,我们对每个计数更改都运行useEffect。这是必要的,因为我们需要始终保持最新的当前值。
useState提供API以更新以前的状态而不捕获当前值。为此,我们需要做的就是向setState提供lambda函数参数。
这段代码可以正确有效地工作。我们在组件的生命周期内使用单个setInterval。卸载组件后,只会调用一次clearInterval。
9. local variable
export default function Demo9() {
console.log('renderDemo9');
const [count, setCount] = useState(0);
let interval = null;
const start = () => {
interval = setInterval(() => {
setCount(c => c + 1);
}, 500);
};
const stop = () => {
clearInterval(interval);
};
return (
<div>
count => {count}
<button onClick={start}>start</button>
<button onClick={stop}>stop</button>
</div>
);
}
我们添加了开始和停止按钮。该代码执行不正确,停止按钮不起作用。在每次渲染期间interval都会创建新的引用,因此本地变量interval会被赋值为null。
10. useRef
export default function Demo10() {
console.log('renderDemo10');
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 500);
};
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<div>
count => {count}
<button onClick={start}>start</button>
<button onClick={stop}>stop</button>
</div>
);
}
如果需要可变变量,那么useRef就相当于一个go-to hook。与局部变量不同,React确保在每次渲染期间都返回相同的引用。
该代码似乎正确,但是有一个细微的错误。如果多次调用start,则将多次调用setInterval触发资源泄漏。
11. 优化useRef
export default function Demo11() {
console.log('renderDemo11');
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const start = () => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 500);
};
const stop = () => {
if (intervalRef.current === null) {
return;
}
clearInterval(intervalRef.current);
intervalRef.current = null;
};
return (
<div>
count => {count}
<button onClick={start}>start</button>
<button onClick={stop}>stop</button>
</div>
);
}
为了避免资源泄漏,如果已经开启了定时器,我们将忽略调用。尽管调用clearInterval(null)不会触发任何错误,但是好的做法是只处理一次资源。
此代码没有资源泄漏,已正确实现,但可能存在性能问题。
memoization是React中主要的性能优化工具。 React.memo进行浅层比较,如果引用相同,则跳过渲染。
如果将start和stop传递给已记忆的组件,则整个优化将失败,因为在每次渲染后都会返回新的引用。
12. useCallback
export default function Demo12() {
console.log('renderDemo12');
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 500);
}, []);
const stop = useCallback(() => {
if (intervalRef.current === null) {
return;
}
clearInterval(intervalRef.current);
intervalRef.current = null;
}, []);
return (
<div>
count => {count}
<button onClick={start}>start</button>
<button onClick={stop}>stop</button>
</div>
);
}
为了使React.memo能够正确完成其工作,我们需要使用useCallback钩子来记住函数。这样,将在每次渲染后函数都提供相同的引用。
该代码没有资源泄漏,可以正确实现,没有性能问题,可以看到即使对于简单的计数器,代码也相当复杂了。
13. 自定义钩子
function useCounter(initialValue, ms) {
const [count, setCount] = useState(initialValue);
const intervalRef = useRef(null);
const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, ms);
}, []);
const stop = useCallback(() => {
if (intervalRef.current === null) {
return;
}
clearInterval(intervalRef.current);
intervalRef.current = null;
}, []);
const reset = useCallback(() => {
setCount(0);
}, []);
return { count, start, stop, reset };
}
为了简化代码,我们需要将所有复杂性封装在useCounter自定义钩子内,并公开干净的api:{count,start,stop,reset}。
export default function Level13() {
console.log('renderLevel13');
const { count, start, stop, reset } = useCounter(0, 500);
return (
<div>
count => {count}
<button onClick={start}>start</button>
<button onClick={stop}>stop</button>
<button onClick={reset}>reset</button>
</div>
);
}
React Hooks雷达

所有的React Hook都是平等的,但是有些钩子比其他的钩子更平等。
✅ Green
绿色钩子是现代React应用程序的主要构建块。他们几乎可以安全地在所有地方使用。
- useReducer
- useState
- useContext
🌕 Yellow
黄色钩子通过使用记忆提供了有用的性能优化。管理生命周期和输入应谨慎进行。
- useCallback
- useMemo
🔴 Red
红钩通过副作用与可变的世界互动。它们功能最强大,应格外小心。对于所有非普通的用例,建议使用自定义钩子。
- useRef
- useEffect
- useLayoutEffect
React Hooks建议

- 遵守hooks规则。
- 不要在主渲染功能中产生任何副作用。
- 取消订阅/处理/销毁所有使用的资源。
- 对于useState,最好使用useReducer或函数更新,以防止在hook中读取和写入相同的值。
- 不要在render函数中使用可变变量,而要使用useRef。
- 如果您保存在useRef中的内容的生命周期比组件本身的生命周期短,请不要忘记在处理资源时取消设置该值。
- 注意无限递归和资源消耗匮乏。
- 在需要时记忆功能和对象以提高性能。
- 正确捕获输入依赖关系(undefined=>每个渲染,[a,b] =>当a或b更改时,[] =>仅一次)。
- 将自定义钩子用于非普通用例。