DrillLab
第 73 / 105 道73 / 105 · #340

自定义 hook 是干什么的,命名有什么约定

What are custom hooks for and what is the naming convention for them

先自己答,再往下看Answer it yourself first

一句话:「带状态的逻辑」抽出来复用。 命名必须以 use 开头

为什么必须 use 开头—— 这是考点,不是风格问题:

  • ESLint 靠这个前缀识别它是 hook, 才能检查 hooks 规则 (react-hooks/rules-of-hooks)。 不加前缀,你在里面违规调用 hook 也不会有人警告你。
  • 它同时也是给读代码的人的信号:这个函数里可能有状态, 所以它有调用位置的限制

关键概念:复用的是逻辑,不是状态。两个组件各自调 useCounter(), 得到的是两份完全独立的状态。 想共享状态得用 Context 或状态库。这一条是高频追问,很多人答错。

什么时候该抽:同一组useState +useEffect 的组合在两处以上出现; 或者一个组件里的 effect 逻辑长到 让主体读不懂了。

会追问:「自定义 hook 能返回什么?」—— 随意。约定是「像 useState 一样返数组」 (调用方好重命名)、 「三个以上返对象」(不用记顺序)。
「里面能调别的 hook 吗?」—— 能,这正是它的意义;但同样要遵守两条规则。

In one line: pull “logic that carries state” out so it can be reused. The name must start with use.

Why the use prefix is mandatory — this is the point being tested, and it is not about style:

  • ESLint uses the prefix to recognise it as a hook so it can enforce the rules of hooks (react-hooks/rules-of-hooks). Without the prefix, you can break those rules inside it and nobody warns you.
  • It is also a signal to whoever reads the code: this function may hold state, so there are limits on where you may call it.

The key idea: you reuse the logic, not the state. Two components that each call useCounter() get two completely independent pieces of state. To share state you need Context or a state library. This is a frequent follow-up and a lot of people get it wrong.

When to extract one: the same combination of useState and useEffect shows up in two or more places; or the effect logic in one component has grown long enough that you can no longer read the component itself.

Follow-up: “What can a custom hook return?” — anything. The convention is “return an array like useState does” (so the caller can rename freely) and “return an object once there are three or more values” (so nobody has to remember the order).
“Can it call other hooks?” — yes, that is the whole point; the same two rules still apply.

JSX自定义 hook 的形状示意Illustrative
1// 一个真实好用的:把「值 + 存 localStorage」打包
2function useLocalStorage(key, initial) {
3 const [value, setValue] = useState(() => {
4 try {
5 const raw = localStorage.getItem(key);
6 return raw ? JSON.parse(raw) : initial;
7 } catch {
8 return initial; // 隐私模式读不了就用默认值
9 }
10 });
11
12 useEffect(() => {
13 try { localStorage.setItem(key, JSON.stringify(value)); } catch {}
14 }, [key, value]);
15
16 return [value, setValue]; // 像 useState 一样返数组
17}
18
19// 用起来
20const [theme, setTheme] = useLocalStorage("theme", "light");
21
22// 注意:两个组件各调一次,得到的是两份独立状态,不是共享的
1// One that is genuinely useful: a value together with storing it in localStorage
2function useLocalStorage(key, initial) {
3 const [value, setValue] = useState(() => {
4 try {
5 const raw = localStorage.getItem(key);
6 return raw ? JSON.parse(raw) : initial;
7 } catch {
8 return initial; // private mode cannot read, so use the default
9 }
10 });
11
12 useEffect(() => {
13 try { localStorage.setItem(key, JSON.stringify(value)); } catch {}
14 }, [key, value]);
15
16 return [value, setValue]; // returns an array, the same shape as useState
17}
18
19// Using it
20const [theme, setTheme] = useLocalStorage("theme", "light");
21
22// Note: two components each calling it get two separate states, not a shared one