Hooks最佳实践 #

一、Hooks 规则 #

1.1 只在最顶层调用 #

jsx
// ❌ 错误:在条件语句中调用
function BadExample({ isLoading }) {
  if (isLoading) {
    const [data, setData] = useState(null); // 错误!
  }
  
  return <div>...</div>;
}

// ✅ 正确:在顶层调用
function GoodExample({ isLoading }) {
  const [data, setData] = useState(null);
  
  if (isLoading) {
    return <Loading />;
  }
  
  return <Data data={data} />;
}

1.2 只在 Preact 函数中调用 #

jsx
// ✅ 正确:在组件中
function Component() {
  const [state, setState] = useState(0);
}

// ✅ 正确:在自定义 Hook 中
function useCustomHook() {
  const [state, setState] = useState(0);
}

// ❌ 错误:在普通函数中
function helper() {
  const [state, setState] = useState(0); // 错误!
}

// ❌ 错误:在类组件中
class Component extends Component {
  method() {
    const [state, setState] = useState(0); // 错误!
  }
}

二、依赖数组 #

2.1 正确设置依赖 #

jsx
// ❌ 错误:缺少依赖
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []); // userId 变化时不会重新获取

  return <div>{user?.name}</div>;
}

// ✅ 正确:包含所有依赖
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]); // userId 变化时重新获取

  return <div>{user?.name}</div>;
}

2.2 依赖检查工具 #

javascript
// .eslintrc.js
module.exports = {
  rules: {
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn'
  }
};

2.3 函数依赖 #

jsx
// ❌ 问题:函数每次渲染都重新创建
function Component({ userId }) {
  const fetchData = () => {
    fetch(`/api/users/${userId}`);
  };

  useEffect(() => {
    fetchData();
  }, [fetchData]); // 每次渲染都会执行
}

// ✅ 解决方案一:将函数放入 useEffect
function Component({ userId }) {
  useEffect(() => {
    const fetchData = () => {
      fetch(`/api/users/${userId}`);
    };
    fetchData();
  }, [userId]);
}

// ✅ 解决方案二:使用 useCallback
function Component({ userId }) {
  const fetchData = useCallback(() => {
    fetch(`/api/users/${userId}`);
  }, [userId]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);
}

// ✅ 解决方案三:函数式更新
function Component() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(newData => {
      setData(prev => ({ ...prev, ...newData }));
    });
  }, []); // 不需要依赖 setData
}

三、性能优化 #

3.1 useMemo 缓存计算 #

jsx
// ❌ 每次渲染都重新计算
function ExpensiveList({ items, filter }) {
  const filteredItems = items.filter(item => 
    item.name.includes(filter)
  ); // 每次渲染都执行

  return <List items={filteredItems} />;
}

// ✅ 使用 useMemo 缓存
function ExpensiveList({ items, filter }) {
  const filteredItems = useMemo(() => {
    console.log('Filtering...');
    return items.filter(item => item.name.includes(filter));
  }, [items, filter]); // 只在依赖变化时重新计算

  return <List items={filteredItems} />;
}

3.2 useCallback 缓存函数 #

jsx
// ❌ 每次渲染创建新函数
function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log('clicked');
  }; // 每次渲染都是新函数

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <Child onClick={handleClick} />
    </div>
  );
}

// ✅ 使用 useCallback
function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []); // 函数引用保持不变

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <Child onClick={handleClick} />
    </div>
  );
}

// 配合 memo 使用
const Child = memo(function Child({ onClick }) {
  console.log('Child rendered');
  return <button onClick={onClick}>Click</button>;
});

3.3 避免过度优化 #

jsx
// ❌ 不必要的 useMemo
function SimpleComponent({ name }) {
  const greeting = useMemo(() => `Hello, ${name}`, [name]);
  return <div>{greeting}</div>;
}

// ✅ 简单计算不需要优化
function SimpleComponent({ name }) {
  return <div>Hello, {name}</div>;
}

// ✅ 复杂计算才需要 useMemo
function ComplexComponent({ data }) {
  const result = useMemo(() => {
    return heavyComputation(data);
  }, [data]);

  return <div>{result}</div>;
}

四、状态管理 #

4.1 状态最小化 #

jsx
// ❌ 冗余状态
function UserForm({ user }) {
  const [name, setName] = useState(user.name);
  const [email, setEmail] = useState(user.email);
  const [fullName, setFullName] = useState(`${user.name} (${user.email})`);
  // fullName 是冗余的

  return <div>{fullName}</div>;
}

// ✅ 派生状态
function UserForm({ user }) {
  const [name, setName] = useState(user.name);
  const [email, setEmail] = useState(user.email);

  const fullName = `${name} (${email})`; // 派生

  return <div>{fullName}</div>;
}

4.2 状态合并 #

jsx
// ❌ 多个相关状态
function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [email, setEmail] = useState('');
  const [phone, setPhone] = useState('');
  // ...
}

// ✅ 合并为对象
function Form() {
  const [form, setForm] = useState({
    firstName: '',
    lastName: '',
    email: '',
    phone: ''
  });

  const updateField = (field, value) => {
    setForm(prev => ({ ...prev, [field]: value }));
  };
}

4.3 状态提升 #

jsx
// 当多个组件需要共享状态时,提升到共同父组件
function App() {
  const [user, setUser] = useState(null);

  return (
    <div>
      <Header user={user} onLogout={() => setUser(null)} />
      <Main user={user} />
      <Footer user={user} />
    </div>
  );
}

五、副作用处理 #

5.1 清理副作用 #

jsx
// ❌ 没有清理
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  }, []); // 内存泄漏!

  return <div>{seconds}</div>;
}

// ✅ 正确清理
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <div>{seconds}</div>;
}

5.2 取消请求 #

jsx
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchUser() {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      
      if (!cancelled) {
        setUser(data);
      }
    }

    fetchUser();

    return () => {
      cancelled = true;
    };
  }, [userId]);

  return <div>{user?.name}</div>;
}

5.3 分离关注点 #

jsx
// ✅ 不同副作用分离到不同的 useEffect
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });

  // 用户数据获取
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  // 窗口大小监听
  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div>{/* ... */}</div>;
}

六、自定义 Hook #

6.1 提取可复用逻辑 #

jsx
// ❌ 重复逻辑
function ComponentA() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/a').then(/* ... */);
  }, []);
}

function ComponentB() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/b').then(/* ... */);
  }, []);
}

// ✅ 提取为自定义 Hook
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url);
        const json = await response.json();
        if (!cancelled) setData(json);
      } catch (e) {
        if (!cancelled) setError(e);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }

    fetchData();
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

function ComponentA() {
  const { data, loading, error } = useFetch('/api/a');
}

function ComponentB() {
  const { data, loading, error } = useFetch('/api/b');
}

6.2 命名约定 #

jsx
// ✅ 以 use 开头
function useWindowSize() { }
function useLocalStorage() { }
function useDebounce() { }

七、常见陷阱 #

7.1 闭包陷阱 #

jsx
// ❌ 闭包陷阱
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // 永远是 0
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// ✅ 使用 ref
function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(countRef.current); // 正确
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// ✅ 使用函数式更新
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => {
        console.log(c); // 正确
        return c;
      });
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

7.2 无限循环 #

jsx
// ❌ 无限循环
function BadComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1); // 触发重新渲染
  }, [count]); // count 变化又触发 effect

  return <div>{count}</div>;
}

// ✅ 避免在 effect 中更新依赖
function GoodComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 只在特定条件下更新
    if (count < 10) {
      setCount(c => c + 1);
    }
  }, [count]);

  return <div>{count}</div>;
}

7.3 对象依赖 #

jsx
// ❌ 每次渲染都是新对象
function Component({ user }) {
  useEffect(() => {
    console.log('User changed');
  }, [{ name: user.name }]); // 每次都是新对象

  return <div>{user.name}</div>;
}

// ✅ 使用原始值
function Component({ user }) {
  useEffect(() => {
    console.log('User changed');
  }, [user.name]); // 原始值

  return <div>{user.name}</div>;
}

// ✅ 或使用 useMemo
function Component({ user }) {
  const userData = useMemo(() => ({ name: user.name }), [user.name]);

  useEffect(() => {
    console.log('User changed');
  }, [userData]);

  return <div>{user.name}</div>;
}

八、总结 #

要点 说明
规则 顶层调用,只在 Preact 函数中
依赖 正确设置,使用 ESLint 检查
性能 合理使用 useMemo/useCallback
副作用 记得清理,分离关注点
状态 最小化,避免冗余

核心原则:

  • 遵循 Hooks 规则
  • 正确管理依赖
  • 及时清理副作用
  • 提取可复用逻辑
  • 避免过度优化
最后更新:2026-03-28