一个自定义Hook彻底改变了我的React开发方式,现在连阿里、字节的前端都在用它
昨天刷脉脉,看到一个腾讯前端发的帖子:*"为什么现在的React代码越写越恶心?"*
底下一片哀嚎。有人说自己的useEffect里嵌套了7层异步调用,有人说每个组件都有30行的loading状态管理...
我笑了。因为一年前的我,也是这样的受害者。
看看你是不是也写过这样的代码:
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
  setLoading(true);
  fetch(url)
    .then((res) => res.json())
    .then(setData)
    .catch(setError)
    .finally(() => setLoading(false));
}, [url]);
恭喜你,你已经掉进了React异步状态管理的"屎坑"。
去年给一个甲方爸爸做项目,他要求页面"秒级响应"。我盯着满屏的loading状态、error处理、try-catch包装器,差点怀疑人生。
凌晨3点,我做了一个决定:干掉所有重复的异步样板代码。
三个小时后,useAsync诞生了。
从此,我的组件长这样:
const { data, error, loading, run } = useAsync();
useEffect(() => {
  run(() => fetchDataFromAPI());
}, []);
甚至更简单:
const { data, loading, error } = useAsync(() => fetchDataFromAPI(), []);
10行代码变成2行,这就是降维打击。
很多人问我,这个Hook到底做了什么黑魔法?
答案很简单:它把所有React开发者都会犯的错误,提前帮你避免了。
让我们剖析一下核心实现:
function useAsync(fn, deps = []) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
// 关键点1:useCallback避免无限重渲染
const run = useCallback(async () => {
    setLoading(true);
    setError(null); // 关键点2:重置错误状态
    
    try {
      const result = await fn();
      setData(result);
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false); // 关键点3:无论如何都要重置loading
    }
  }, deps);
  useEffect(() => {
    if (fn) run();
  }, [run]);
return { data, error, loading, run };
}
这就是为什么大厂面试官喜欢考察自定义Hook的原因——它体现了你对React机制的深度理解。
评论区总有人问:*"既然有React Query、SWR这些成熟方案,为什么还要重复造轮子?"*
这个问题问得好。让我告诉你真相:
工具没有对错,只有合适不合适。
在字节的时候,我见过用React Query管理一个简单表单提交的代码——7个文件,200行配置,就为了发一个POST请求。
这不是工程化,这是过度工程化。
让我用一个真实案例展示这个Hook的威力。
改造前(屎山版):
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [updating, setUpdating] = useState(false);
const [updateError, setUpdateError] = useState(null);
  useEffect(() => {
    setLoading(true);
    setError(null);
    fetchUser(userId)
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [userId]);
const handleUpdate = async (data) => {
    setUpdating(true);
    setUpdateError(null);
    try {
      const updatedUser = await updateUser(userId, data);
      setUser(updatedUser);
    } catch (err) {
      setUpdateError(err);
    } finally {
      setUpdating(false);
    }
  };
if (loading) return<div>Loading...</div>;
if (error) return<div>Error: {error.message}</div>;
return (
    <div>
      <h1>{user?.name}</h1>
      <button 
        onClick={() => handleUpdate({...})} 
        disabled={updating}
      >
        {updating ? 'Updating...' : 'Update'}
      </button>
      {updateError && <div>Update failed: {updateError.message}</div>}
    </div>
  );
}
改造后(艺术品版):
function UserProfile({ userId }) {
const { 
    data: user, 
    loading, 
    error 
  } = useAsync(() => fetchUser(userId), [userId]);
const { 
    loading: updating, 
    error: updateError, 
    run: updateUser 
  } = useAsync();
const handleUpdate = (data) => {
    updateUser(() => updateUser(userId, data));
  };
if (loading) return<div>Loading...</div>;
if (error) return<div>Error: {error.message}</div>;
return (
    <div>
      <h1>{user?.name}</h1>
      <button onClick={() => handleUpdate({...})} disabled={updating}>
        {updating ? 'Updating...' : 'Update'}
      </button>
      {updateError && <div>Update failed: {updateError.message}</div>}
    </div>
  );
}
从40行压缩到25行,逻辑更清晰,可读性翻倍。
基础版本已经能满足80%的场景,但如果你想要更极致的体验,这里有几个进阶优化:
function useAsync(fn, deps = []) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const cancelRef = useRef();
const run = useCallback(async () => {
    // 取消上一次请求
    cancelRef.current?.cancel();
    
    const cancelToken = { cancel: () => {} };
    cancelRef.current = cancelToken;
    
    setLoading(true);
    setError(null);
    
    try {
      const result = await fn(cancelToken);
      if (!cancelToken.cancelled) {
        setData(result);
      }
    } catch (err) {
      if (!cancelToken.cancelled) {
        setError(err);
      }
    } finally {
      if (!cancelToken.cancelled) {
        setLoading(false);
      }
    }
  }, deps);
  useEffect(() => {
    return() => cancelRef.current?.cancel();
  }, []);
// 其他逻辑...
}
const run = useCallback(async (optimisticData) => {
  if (optimisticData) {
    setData(optimisticData);
  }
  
  // 执行真实请求...
}, deps);
经过一年多的实践,我总结出几个不适用的场景:
记住:银弹不存在,合适的工具解决合适的问题。
有人说我有代码洁癖。
我承认。
看到重复的样板代码,我浑身难受。看到意大利面条式的useEffect,我夜不能寐。
但这种"洁癖"让我成为了更好的开发者。
useAsync只是开始。当你开始思考如何抽象重复逻辑、如何让代码更优雅时,你就已经从"代码民工"进化成了"代码艺术家"。
你的项目里还在用着什么"屎山代码"?
有没有被这种重复的异步状态管理折磨过?
还是说,你也有自己的"银弹Hook"想分享?
评论区见真章。顺便点个赞,让更多被异步状态折磨的同行看到这个解决方案。
毕竟,拯救一个程序员的理智,功德无量。