讲解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 在正确的时机执行,避免不必要的重复执行和内存泄漏。

相关推荐
渣哥19 小时前
Spring 创建 Bean 的多种方式对比与最佳实践
前端·javascript·面试
Copper peas19 小时前
axios使用过程
前端·javascript·vue.js
云鹤_19 小时前
【Amis源码阅读】如何将json配置渲染成页面?
前端·低代码
逛逛GitHub19 小时前
飞书多维表格 + 即梦 4.0,打造你的 AI 生图游乐场。
前端·github
行走在顶尖19 小时前
Vue3 基础笔记
前端
guoss19 小时前
实现渐变背景叠加渐变圆角边框
前端
枫,为落叶19 小时前
【vue】导出excel
前端·vue.js·excel
转转技术团队19 小时前
当 AI 走进前端开发:代理插件的全流程开发实践
前端·javascript·ai编程
慧一居士19 小时前
Quill 富文本编辑器 功能介绍,使用场景说明,使用示例演示
前端