自定义Hooks #

一、自定义Hooks概述 #

1.1 什么是自定义Hook #

自定义 Hook 是一个以 use 开头的函数,可以调用其他 Hook。

javascript
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

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

  return size;
}

1.2 为什么使用自定义Hook #

优势 说明
逻辑复用 在多个组件中复用相同逻辑
代码组织 将复杂逻辑抽离成独立模块
关注分离 组件关注UI,Hook关注逻辑
可测试 独立的逻辑更易于测试

1.3 命名约定 #

javascript
// ✅ 正确:以use开头
function useWindowSize() {}
function useFetch() {}
function useLocalStorage() {}

// ❌ 错误:不以use开头
function getWindowSize() {}
function fetch() {}

二、常用自定义Hooks #

2.1 useLocalStorage #

javascript
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setStoredValue = useCallback((value) => {
    try {
      const valueToStore = value instanceof Function ? value(value) : value;
      setValue(valueToStore);
      localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  }, [key]);

  return [value, setStoredValue];
}

// 使用
function App() {
  const [name, setName] = useLocalStorage('name', '');

  return (
    <input
      value={name}
      onChange={(e) => setName(e.target.value)}
      placeholder="输入名字"
    />
  );
}

2.2 useFetch #

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

  useEffect(() => {
    let isMounted = true;
    const controller = new AbortController();

    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url, { signal: controller.signal });
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const json = await response.json();
        
        if (isMounted) {
          setData(json);
          setError(null);
        }
      } catch (err) {
        if (isMounted && err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      isMounted = false;
      controller.abort();
    };
  }, [url]);

  return { data, loading, error };
}

// 使用
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

2.3 useToggle #

javascript
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  return { value, toggle, setTrue, setFalse };
}

// 使用
function Modal() {
  const { value: isOpen, toggle, setFalse: close } = useToggle();

  return (
    <>
      <button onClick={toggle}>打开弹窗</button>
      {isOpen && (
        <div className="modal">
          <p>弹窗内容</p>
          <button onClick={close}>关闭</button>
        </div>
      )}
    </>
  );
}

2.4 useDebounce #

javascript
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

// 使用
function Search() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      search(debouncedQuery);
    }
  }, [debouncedQuery]);

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="搜索..."
    />
  );
}

2.5 useWindowSize #

javascript
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

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

  return size;
}

// 使用
function ResponsiveComponent() {
  const { width } = useWindowSize();

  return (
    <div>
      {width < 768 ? <MobileLayout /> : <DesktopLayout />}
    </div>
  );
}

2.6 usePrevious #

javascript
function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

// 使用
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>当前: {count}</p>
      <p>之前: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

2.7 useClickOutside #

javascript
function useClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };

    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

// 使用
function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const ref = useRef();

  useClickOutside(ref, () => setIsOpen(false));

  return (
    <div ref={ref}>
      <button onClick={() => setIsOpen(!isOpen)}>切换</button>
      {isOpen && <div className="dropdown">下拉内容</div>}
    </div>
  );
}

2.8 useKeyPress #

javascript
function useKeyPress(targetKey) {
  const [pressed, setPressed] = useState(false);

  useEffect(() => {
    const downHandler = ({ key }) => {
      if (key === targetKey) {
        setPressed(true);
      }
    };

    const upHandler = ({ key }) => {
      if (key === targetKey) {
        setPressed(false);
      }
    };

    window.addEventListener('keydown', downHandler);
    window.addEventListener('keyup', upHandler);

    return () => {
      window.removeEventListener('keydown', downHandler);
      window.removeEventListener('keyup', upHandler);
    };
  }, [targetKey]);

  return pressed;
}

// 使用
function Game() {
  const upPressed = useKeyPress('ArrowUp');
  const downPressed = useKeyPress('ArrowDown');

  return (
    <div>
      <p>上: {upPressed ? '按下' : '未按下'}</p>
      <p>下: {downPressed ? '按下' : '未按下'}</p>
    </div>
  );
}

三、高级自定义Hooks #

3.1 useAsync #

javascript
function useAsync(asyncFunction, immediate = true) {
  const [state, setState] = useState({
    loading: immediate,
    data: null,
    error: null
  });

  const execute = useCallback(async (...params) => {
    setState({ loading: true, data: null, error: null });
    
    try {
      const data = await asyncFunction(...params);
      setState({ loading: false, data, error: null });
      return data;
    } catch (error) {
      setState({ loading: false, data: null, error });
      throw error;
    }
  }, [asyncFunction]);

  useEffect(() => {
    if (immediate) {
      execute();
    }
  }, [execute, immediate]);

  return { ...state, execute };
}

// 使用
function UserList() {
  const { data: users, loading, error, execute } = useAsync(
    () => fetch('/api/users').then(res => res.json())
  );

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error.message}</div>;

  return (
    <div>
      <button onClick={execute}>刷新</button>
      <ul>
        {users?.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

3.2 useForm #

javascript
function useForm(initialValues, validate) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const handleChange = useCallback((e) => {
    const { name, value, type, checked } = e.target;
    setValues(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));
  }, []);

  const handleBlur = useCallback((e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
    
    if (validate) {
      const validationErrors = validate(values);
      setErrors(validationErrors);
    }
  }, [values, validate]);

  const handleSubmit = useCallback((onSubmit) => (e) => {
    e.preventDefault();
    
    if (validate) {
      const validationErrors = validate(values);
      setErrors(validationErrors);
      setTouched(
        Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {})
      );
      
      if (Object.keys(validationErrors).length > 0) {
        return;
      }
    }
    
    onSubmit(values);
  }, [values, validate]);

  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    handleSubmit,
    reset
  };
}

// 使用
function LoginForm() {
  const {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    handleSubmit,
    reset
  } = useForm(
    { email: '', password: '' },
    (values) => {
      const errors = {};
      if (!values.email) errors.email = '邮箱必填';
      if (!values.password) errors.password = '密码必填';
      return errors;
    }
  );

  const onSubmit = (data) => {
    console.log('提交:', data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          name="email"
          value={values.email}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.email && errors.email && <span>{errors.email}</span>}
      </div>
      <div>
        <input
          name="password"
          type="password"
          value={values.password}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.password && errors.password && <span>{errors.password}</span>}
      </div>
      <button type="submit">登录</button>
      <button type="button" onClick={reset}>重置</button>
    </form>
  );
}

3.3 useIntersectionObserver #

javascript
function useIntersectionObserver(options = {}) {
  const [entry, setEntry] = useState(null);
  const [node, setNode] = useState(null);

  const observer = useRef(null);

  useEffect(() => {
    if (observer.current) {
      observer.current.disconnect();
    }

    observer.current = new IntersectionObserver(([entry]) => {
      setEntry(entry);
    }, options);

    if (node) {
      observer.current.observe(node);
    }

    return () => {
      if (observer.current) {
        observer.current.disconnect();
      }
    };
  }, [node, options.threshold, options.root, options.rootMargin]);

  return [setNode, entry];
}

// 使用
function LazyImage({ src, alt }) {
  const [setRef, entry] = useIntersectionObserver({
    threshold: 0.1
  });
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    if (entry?.isIntersecting && !loaded) {
      setLoaded(true);
    }
  }, [entry?.isIntersecting, loaded]);

  return (
    <div ref={setRef}>
      {loaded ? (
        <img src={src} alt={alt} />
      ) : (
        <div className="placeholder">加载中...</div>
      )}
    </div>
  );
}

四、最佳实践 #

4.1 单一职责 #

javascript
// ✅ 好的实践:每个Hook只做一件事
function useUserName(userId) {
  const { data } = useFetch(`/api/users/${userId}`);
  return data?.name;
}

function useUserEmail(userId) {
  const { data } = useFetch(`/api/users/${userId}`);
  return data?.email;
}

// ❌ 不好的实践:一个Hook做太多事
function useUserData(userId) {
  // 获取用户数据、处理权限、发送统计...
}

4.2 返回值设计 #

javascript
// 数组返回值(适合简单场景)
const [value, toggle] = useToggle();

// 对象返回值(适合复杂场景)
const { data, loading, error, refetch } = useFetch(url);

// 返回多个值
const { value, setValue, reset, hasChanges } = useFormState(initialValue);

4.3 参数设计 #

javascript
// 使用对象参数,便于扩展
function useFetch(url, options = {}) {
  const {
    method = 'GET',
    headers = {},
    body = null,
    immediate = true
  } = options;
  
  // ...
}

// 使用
useFetch('/api/users', {
  method: 'POST',
  body: { name: 'Alice' }
});

五、总结 #

要点 说明
命名 以use开头
单一职责 每个Hook只做一件事
返回值 根据复杂度选择数组或对象
参数 使用对象参数便于扩展

核心原则:

  • 自定义 Hook 用于复用逻辑
  • 遵循 Hook 规则
  • 保持 Hook 简单和专注
  • 提供清晰的 API
最后更新:2026-03-26