useEffect #

一、useEffect概述 #

1.1 什么是副作用 #

副作用是指函数组件中与渲染无关的操作:

text
副作用类型
├── 数据获取        API 请求
├── 订阅            事件监听
├── 定时器          setTimeout/setInterval
├── DOM 操作        直接操作 DOM
└── 存储            localStorage/sessionStorage

1.2 基本语法 #

jsx
useEffect(effectFunction, dependencies);
参数 说明
effectFunction 副作用函数
dependencies 依赖数组(可选)

二、基本用法 #

2.1 每次渲染后执行 #

jsx
import { useEffect, useState } from 'preact/hooks';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('组件渲染后执行');
  });

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

2.2 仅挂载时执行 #

jsx
function Example() {
  useEffect(() => {
    console.log('组件挂载时执行一次');
  }, []); // 空依赖数组

  return <div>Example</div>;
}

2.3 依赖变化时执行 #

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

  useEffect(() => {
    console.log('userId 变化:', userId);
    fetchUser(userId).then(setUser);
  }, [userId]); // userId 变化时执行

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

三、清理副作用 #

3.1 清理函数 #

jsx
function Timer() {
  const [seconds, setSeconds] = useState(0);

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

    // 返回清理函数
    return () => {
      clearInterval(interval);
      console.log('定时器已清理');
    };
  }, []);

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

3.2 清理时机 #

jsx
function Example() {
  useEffect(() => {
    console.log('副作用执行');
    
    return () => {
      console.log('清理函数执行');
    };
  });

  return <div>Example</div>;
}

// 执行顺序:
// 1. 副作用执行
// 2. 组件更新时:先执行清理 → 再执行副作用
// 3. 组件卸载时:执行清理

3.3 清理订阅 #

jsx
function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = connectToRoom(roomId);
    
    connection.on('message', (msg) => {
      console.log('New message:', msg);
    });

    return () => {
      connection.disconnect();
    };
  }, [roomId]);

  return <div>Room: {roomId}</div>;
}

四、数据获取 #

4.1 基本数据获取 #

jsx
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
          throw new Error('Failed to fetch');
        }
        
        const data = await response.json();
        setUser(data);
        setError(null);
      } catch (e) {
        setError(e.message);
      } finally {
        setLoading(false);
      }
    }

    fetchUser();
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <div>{user.name}</div>;
}

4.2 取消请求 #

jsx
function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    const controller = new AbortController();
    
    async function search() {
      try {
        const response = await fetch(
          `/api/search?q=${query}`,
          { signal: controller.signal }
        );
        const data = await response.json();
        setResults(data);
      } catch (e) {
        if (e.name !== 'AbortError') {
          console.error(e);
        }
      }
    }

    if (query) {
      search();
    }

    return () => {
      controller.abort();
    };
  }, [query]);

  return (
    <ul>
      {results.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

4.3 防止竞态条件 #

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.1 窗口事件 #

jsx
function WindowSize() {
  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 (
    <div>
      {size.width} x {size.height}
    </div>
  );
}

5.2 键盘事件 #

jsx
function KeyboardShortcuts() {
  useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.key === 'Escape') {
        console.log('Escape pressed');
      }
      if ((e.ctrlKey || e.metaKey) && e.key === 's') {
        e.preventDefault();
        console.log('Save triggered');
      }
    };

    document.addEventListener('keydown', handleKeyDown);

    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, []);

  return <div>Press keys to see shortcuts</div>;
}

5.3 自定义事件 #

jsx
function EventEmitter() {
  useEffect(() => {
    const handleCustomEvent = (e) => {
      console.log('Custom event:', e.detail);
    };

    document.addEventListener('myEvent', handleCustomEvent);

    return () => {
      document.removeEventListener('myEvent', handleCustomEvent);
    };
  }, []);

  const emitEvent = () => {
    document.dispatchEvent(new CustomEvent('myEvent', {
      detail: { message: 'Hello' }
    }));
  };

  return <button onClick={emitEvent}>Emit Event</button>;
}

六、定时器 #

6.1 setInterval #

jsx
function Clock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const timer = setInterval(() => {
      setTime(new Date());
    }, 1000);

    return () => {
      clearInterval(timer);
    };
  }, []);

  return <div>{time.toLocaleTimeString()}</div>;
}

6.2 setTimeout #

jsx
function Notification({ message, duration = 3000, onClose }) {
  const [visible, setVisible] = useState(true);

  useEffect(() => {
    const timer = setTimeout(() => {
      setVisible(false);
      onClose?.();
    }, duration);

    return () => {
      clearTimeout(timer);
    };
  }, [duration, onClose]);

  if (!visible) return null;

  return (
    <div class="notification">
      {message}
    </div>
  );
}

6.3 自定义 useInterval Hook #

jsx
function useInterval(callback, delay) {
  const savedCallback = useRef(callback);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay === null) return;

    const id = setInterval(() => {
      savedCallback.current();
    }, delay);

    return () => clearInterval(id);
  }, [delay]);
}

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

  useInterval(() => {
    setCount(c => c + 1);
  }, 1000);

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

七、DOM 操作 #

7.1 直接 DOM 操作 #

jsx
function FocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} />;
}

7.2 测量 DOM #

jsx
function MeasureElement() {
  const ref = useRef(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    if (ref.current) {
      const { width, height } = ref.current.getBoundingClientRect();
      setDimensions({ width, height });
    }
  }, []);

  return (
    <div>
      <div ref={ref} style={{ width: '200px', height: '100px', background: '#f0f0f0' }}>
        Content
      </div>
      <p>Width: {dimensions.width}, Height: {dimensions.height}</p>
    </div>
  );
}

7.3 动态样式 #

jsx
function DynamicStyle({ isHighlighted }) {
  const ref = useRef(null);

  useEffect(() => {
    if (ref.current) {
      ref.current.style.backgroundColor = isHighlighted ? 'yellow' : 'white';
    }
  }, [isHighlighted]);

  return <div ref={ref}>Content</div>;
}

八、本地存储 #

8.1 localStorage #

jsx
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// 使用
function TodoApp() {
  const [todos, setTodos] = useLocalStorage('todos', []);

  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
    </div>
  );
}

8.2 sessionStorage #

jsx
function useSessionStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = sessionStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    sessionStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

九、依赖数组 #

9.1 依赖规则 #

jsx
// 无依赖:每次渲染都执行
useEffect(() => {
  console.log('每次渲染');
});

// 空数组:仅挂载时执行
useEffect(() => {
  console.log('仅挂载时');
}, []);

// 有依赖:依赖变化时执行
useEffect(() => {
  console.log('count 变化:', count);
}, [count]);

// 多个依赖
useEffect(() => {
  console.log('a 或 b 变化');
}, [a, b]);

9.2 常见错误 #

jsx
function Example({ userId }) {
  const [data, setData] = useState(null);

  // 错误:缺少依赖
  useEffect(() => {
    fetchUser(userId).then(setData);
  }, []); // userId 变化时不会重新获取

  // 正确:包含所有依赖
  useEffect(() => {
    fetchUser(userId).then(setData);
  }, [userId]);
}

9.3 ESLint 规则 #

javascript
// .eslintrc.js
{
  "rules": {
    "react-hooks/exhaustive-deps": "warn"
  }
}

十、useLayoutEffect #

10.1 与 useEffect 的区别 #

Hook 执行时机 用途
useEffect 渲染后异步执行 大多数副作用
useLayoutEffect DOM 更新后同步执行 DOM 测量/操作

10.2 使用场景 #

jsx
import { useLayoutEffect, useRef, useState } from 'preact/hooks';

function Tooltip({ content }) {
  const ref = useRef(null);
  const [position, setPosition] = useState({ top: 0, left: 0 });

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setPosition({ top: -height - 10, left: 0 });
  }, [content]);

  return (
    <div 
      ref={ref} 
      style={{ position: 'absolute', top: position.top, left: position.left }}
    >
      {content}
    </div>
  );
}

十一、最佳实践 #

11.1 分离关注点 #

jsx
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>;
}

11.2 提取自定义 Hook #

jsx
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 ? <MobileView /> : <DesktopView />}
    </div>
  );
}

十二、总结 #

要点 说明
执行时机 渲染后执行
清理函数 返回函数,卸载或更新前执行
依赖数组 控制执行时机
数据获取 注意取消请求和竞态条件
事件监听 记得清理

核心原则:

  • 正确设置依赖数组
  • 清理副作用避免内存泄漏
  • 分离不同关注点
  • 提取可复用逻辑为自定义 Hook
最后更新:2026-03-28