讲解react的useEffect用法,被监听字段的注意事项

React useEffect 深度讲解

useEffect 基本概念

useEffect 是 React Hooks 中用于处理副作用的钩子函数,它模拟了类组件中的生命周期方法。

jsx

javascript 复制代码
import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // 副作用逻辑
    console.log('组件渲染或更新');
    
    // 清理函数(可选)
    return () => {
      console.log('清理工作');
    };
  }, [dependencies]); // 依赖数组
}

依赖数组的详细解析

1. 空依赖数组 []

jsx

scss 复制代码
useEffect(() => {
  // 只在组件挂载时执行一次
  console.log('组件挂载');
}, []);

2. 包含依赖的数组

jsx

scss 复制代码
const [count, setCount] = useState(0);
const [name, setName] = useState('');

useEffect(() => {
  // 当 count 或 name 变化时执行
  console.log(`Count: ${count}, Name: ${name}`);
}, [count, name]);

3. 无依赖数组

jsx

javascript 复制代码
useEffect(() => {
  // 每次渲染后都会执行
  console.log('组件渲染');
});

应该被监听的字段

必须监听的字段

1. State 变量

jsx

scss 复制代码
const [user, setUser] = useState({ id: '', name: '', age: 0 });
const [items, setItems] = useState([]);

useEffect(() => {
  // 当 user 变化时执行
  fetchUserData(user.id);
}, [user.id]); // ✅ 正确:监听 state

useEffect(() => {
  // 当 items 变化时执行
  localStorage.setItem('items', JSON.stringify(items));
}, [items]);

2. Props

jsx

scss 复制代码
function UserProfile({ userId, onUpdate }) {
  useEffect(() => {
    // 当 userId prop 变化时执行
    fetchUserProfile(userId);
  }, [userId]); // ✅ 正确:监听 prop

  useEffect(() => {
    // 当 onUpdate prop 变化时执行(函数引用)
    setupUpdateListener(onUpdate);
  }, [onUpdate]);
}

3. 由其他 Hooks 返回的值

jsx

scss 复制代码
const theme = useContext(ThemeContext);
const location = useLocation(); // React Router
const dispatch = useDispatch(); // Redux

useEffect(() => {
  // 当 theme 变化时执行
  applyTheme(theme);
}, [theme]);

需要谨慎处理的字段

1. 对象和数组

jsx

scss 复制代码
const [user, setUser] = useState({ name: 'John', age: 25 });

useEffect(() => {
  // ❌ 错误:每次渲染都会执行,因为对象引用不同
  console.log('User changed');
}, [user]);

// ✅ 正确:监听具体属性
useEffect(() => {
  console.log('User name changed');
}, [user.name]);

// ✅ 或者使用 useMemo 优化
const userMemo = useMemo(() => user, [user.name, user.age]);
useEffect(() => {
  console.log('User changed');
}, [userMemo]);

2. 函数

jsx

scss 复制代码
function MyComponent({ onSubmit }) {
  const [data, setData] = useState('');
  
  // ❌ 错误:每次渲染都会创建新函数
  const handleProcess = () => {
    processData(data);
  };
  
  useEffect(() => {
    handleProcess();
  }, [handleProcess]); // 依赖总是变化
  
  // ✅ 正确:使用 useCallback
  const handleProcess = useCallback(() => {
    processData(data);
  }, [data]);
  
  useEffect(() => {
    handleProcess();
  }, [handleProcess]);
  
  // ✅ 或者将函数定义在 useEffect 内部
  useEffect(() => {
    const processData = () => {
      // 处理数据
    };
    processData();
  }, [data]);
}

除了 state 和 props,其他变量监听的意义

1. 来自 Context 的值

jsx

scss 复制代码
import { useContext, useEffect } from 'react';
import { AuthContext, ThemeContext } from './context';

function MyComponent() {
  const { user } = useContext(AuthContext);
  const theme = useContext(ThemeContext);
  
  useEffect(() => {
    // 当 Context 值变化时执行
    if (user) {
      loadUserPreferences(user.id);
    }
  }, [user]); // ✅ 有意义:监听 Context 值
  
  useEffect(() => {
    document.body.className = theme;
  }, [theme]);
}

2. URL 参数和路由状态

jsx

scss 复制代码
import { useParams, useLocation } from 'react-router-dom';

function ProductPage() {
  const { productId } = useParams();
  const location = useLocation();
  
  useEffect(() => {
    // 当 URL 参数变化时执行
    fetchProduct(productId);
  }, [productId]); // ✅ 有意义:监听路由参数
  
  useEffect(() => {
    // 当路由位置变化时执行
    trackPageView(location.pathname);
  }, [location]);
}

3. 来自 Redux 或其他状态管理的值

jsx

scss 复制代码
import { useSelector, useDispatch } from 'react-redux';

function Cart() {
  const cartItems = useSelector(state => state.cart.items);
  const total = useSelector(state => state.cart.total);
  
  useEffect(() => {
    // 当 Redux 状态变化时执行
    updateCartSummary(cartItems);
  }, [cartItems]); // ✅ 有意义:监听 Redux 状态
}

4. 计算值(使用 useMemo)

jsx

scss 复制代码
function TodoList({ todos, filter }) {
  const filteredTodos = useMemo(() => {
    return todos.filter(todo => 
      todo.text.toLowerCase().includes(filter.toLowerCase())
    );
  }, [todos, filter]);
  
  useEffect(() => {
    // 当计算值变化时执行
    console.log('Filtered todos updated:', filteredTodos.length);
  }, [filteredTodos]); // ✅ 有意义:监听计算值
}

5. Refs(通常不需要监听)

jsx

scss 复制代码
function MyComponent() {
  const inputRef = useRef(null);
  const previousValue = useRef('');
  
  useEffect(() => {
    // ❌ 通常不需要监听 ref
    // ref 的 .current 属性变化不会触发重新渲染
  }, [inputRef]); // 无意义
  
  // ✅ 正确的 ref 使用方式
  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []); // 只在挂载时执行
}

6. 外部变量(需要谨慎)

jsx

scss 复制代码
let externalCounter = 0; // 模块级别的变量

function MyComponent() {
  useEffect(() => {
    // ❌ 危险:外部变量变化不会触发重新渲染
    console.log('External counter:', externalCounter);
  }, [externalCounter]); // 依赖不会变化
  
  // ✅ 更好的方式:使用 state 或 ref
  const [counter, setCounter] = useState(externalCounter);
  
  useEffect(() => {
    const interval = setInterval(() => {
      externalCounter++;
      setCounter(externalCounter); // 触发重新渲染
    }, 1000);
    
    return () => clearInterval(interval);
  }, []);
}

最佳实践和常见模式

1. 依赖项完整性

jsx

scss 复制代码
function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');
  
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();
    
    // ✅ 正确:在 effect 内部定义函数
    const handleMessage = (msg) => {
      showNotification('New message: ' + msg);
    };
    
    connection.on('message', handleMessage);
    
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // 所有依赖都声明
}

2. 使用自定义 Hook 封装复杂逻辑

jsx

javascript 复制代码
// 自定义 Hook
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    let cancelled = false;
    
    async function fetchData() {
      setLoading(true);
      try {
        const response = await fetch(url);
        const result = await response.json();
        if (!cancelled) {
          setData(result);
        }
      } catch (error) {
        if (!cancelled) {
          console.error('Fetch error:', error);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }
    
    fetchData();
    
    return () => {
      cancelled = true;
    };
  }, [url]); // url 变化时重新获取
  
  return { data, loading };
}

// 使用自定义 Hook
function UserProfile({ userId }) {
  const { data: user, loading } = useApi(`/api/users/${userId}`);
  
  if (loading) return <div>Loading...</div>;
  return <div>{user.name}</div>;
}

3. 避免无限循环

jsx

scss 复制代码
function UserProfile() {
  const [user, setUser] = useState({ name: '', count: 0 });
  
  // ❌ 危险:可能导致无限循环
  useEffect(() => {
    setUser(prev => ({ ...prev, count: prev.count + 1 }));
  }, [user]); // user 变化触发 effect,effect 又更新 user
  
  // ✅ 正确:只在必要时更新
  useEffect(() => {
    if (someCondition) {
      setUser(prev => ({ ...prev, count: prev.count + 1 }));
    }
  }, [someCondition]);
}

总结

应该监听的字段:

  • State 变量
  • Props
  • Context 值
  • 路由参数和状态
  • 来自状态管理的值
  • 使用 useMemo 的计算值

监听意义有限的字段:

  • Refs(通常不需要)
  • 模块级别变量
  • 在 effect 内部定义的变量

关键原则:

  1. 只监听那些可能变化并影响副作用的变量
  2. 保持依赖数组的完整性
  3. 对于对象和数组,考虑监听具体属性或使用 useMemo
  4. 对于函数,使用 useCallback 或将其定义在 effect 内部
  5. 使用自定义 Hook 封装复杂的效果逻辑

通过合理使用依赖数组,可以确保 useEffect 在正确的时机执行,避免不必要的重复执行和内存泄漏。

相关推荐
Jonathan Star6 小时前
沉浸式雨天海岸:用A-Frame打造WebXR互动场景
前端·javascript
工业甲酰苯胺6 小时前
实现 json path 来评估函数式解析器的损耗
java·前端·json
老前端的功夫6 小时前
Web应用的永生之术:PWA落地与实践深度指南
java·开发语言·前端·javascript·css·node.js
LilySesy7 小时前
ABAP+WHERE字段长度不一致报错解决
java·前端·javascript·bug·sap·abap·alv
Wang's Blog8 小时前
前端FAQ: Vue 3 与 Vue 2 相⽐有哪些重要的改进?
前端·javascript·vue.js
再希8 小时前
React+Tailwind CSS+Shadcn UI
前端·react.js·ui
用户47949283569158 小时前
JavaScript 的 NaN !== NaN 之谜:从 CPU 指令到 IEEE 754 标准的完整解密
前端·javascript
群联云防护小杜8 小时前
国产化环境下 Web 应用如何满足等保 2.0?从 Nginx 配置到 AI 防护实战
运维·前端·nginx
醉方休9 小时前
Web3.js 全面解析
前端·javascript·electron
前端开发爱好者9 小时前
前端新玩具:Vike 发布!
前端·javascript