最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

從閉包陷阱到內(nèi)存泄漏解析React Hooks避坑指南

 更新時(shí)間:2026年03月16日 08:49:44   作者:兆子龍  
這篇文中主要從閉包陷阱到依賴地獄,從無限循環(huán)到內(nèi)存泄漏,為大家簡單盤點(diǎn)一下React Hooks中的常見避坑方法,文中的示例代碼講解詳細(xì),希望對大家有所幫助

一、凌晨三點(diǎn)的 Bug

上周五晚上十點(diǎn),準(zhǔn)備下班。突然測試同學(xué)發(fā)來消息:「你這個(gè)計(jì)數(shù)器有問題,點(diǎn)了半天還是 0?!?/p>

我心想,一個(gè)計(jì)數(shù)器能有什么問題?打開代碼一看:

function Counter() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setTimeout(() => {
      setCount(count + 1);
    }, 3000);
  };
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>+1 (延遲 3 秒)</button>
    </div>
  );
}

看起來沒問題啊。但實(shí)際運(yùn)行時(shí),快速點(diǎn)擊 5 次按鈕,3 秒后 count 只變成 1,而不是 5。

這就是 Hooks 的第一個(gè)大坑:閉包陷阱

那天晚上我 debug 到凌晨三點(diǎn),才把所有 Hooks 的坑都踩了一遍。今天就來聊聊這些讓人頭禿的陷阱,以及怎么避開它們。

二、閉包陷阱:useState 的「時(shí)光機(jī)」

2.1 問題重現(xiàn)

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

  const handleClick = () => {
    setTimeout(() => {
      setCount(count + 1);  // 這里的 count 是點(diǎn)擊時(shí)的值
    }, 3000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>+1 (延遲 3 秒)</button>
    </div>
  );
}

現(xiàn)象

  1. 快速點(diǎn)擊 5 次按鈕
  2. 3 秒后,count 只變成 1(而不是 5)

原因

每次點(diǎn)擊時(shí),handleClick 函數(shù)都會(huì)捕獲當(dāng)時(shí)的 count 值(閉包)。5 次點(diǎn)擊時(shí) count 都是 0,所以 5 個(gè) setTimeout 都執(zhí)行 setCount(0 + 1),最終結(jié)果是 1。

2.2 解決方案

方案 1:函數(shù)式更新

const handleClick = () => {
  setTimeout(() => {
    setCount(prevCount => prevCount + 1);  // 使用最新的 count
  }, 3000);
};

方案 2:使用 useRef

function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  const handleClick = () => {
    setTimeout(() => {
      setCount(countRef.current + 1);
    }, 3000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>+1 (延遲 3 秒)</button>
    </div>
  );
}

方案 3:使用 useReducer

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  const handleClick = () => {
    setTimeout(() => {
      dispatch({ type: 'increment' });  // 不依賴閉包
    }, 3000);
  };

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={handleClick}>+1 (延遲 3 秒)</button>
    </div>
  );
}

2.3 更隱蔽的閉包陷阱

function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const handleSearch = () => {
    fetchResults(query).then(data => {
      setResults(data);
    });
  };

  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Current query:', query);  // 永遠(yuǎn)打印初始值
    }, 1000);

    return () => clearInterval(timer);
  }, []);  // 空依賴數(shù)組導(dǎo)致閉包

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}

問題setInterval 里的 query 永遠(yuǎn)是初始值(空字符串)。

原因useEffect 的依賴數(shù)組是空的,effect 只執(zhí)行一次,閉包捕獲的是初始的 query

解決

useEffect(() => {
  const timer = setInterval(() => {
    console.log('Current query:', query);
  }, 1000);

  return () => clearInterval(timer);
}, [query]);  // 添加 query 依賴

或者用 useRef

const queryRef = useRef(query);

useEffect(() => {
  queryRef.current = query;
}, [query]);

useEffect(() => {
  const timer = setInterval(() => {
    console.log('Current query:', queryRef.current);
  }, 1000);

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

三、useEffect 的依賴地獄

3.1 缺失依賴導(dǎo)致的 Bug

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

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []);  // 缺少 userId 依賴

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

問題userId 變化時(shí),不會(huì)重新獲取用戶數(shù)據(jù)。

解決

useEffect(() => {
  fetchUser(userId).then(setUser);
}, [userId]);  // 添加 userId 依賴

3.2 對象/數(shù)組依賴導(dǎo)致的無限循環(huán)

function DataList() {
  const [data, setData] = useState([]);
  const filters = { status: 'active', type: 'user' };  // 每次渲染都是新對象

  useEffect(() => {
    fetchData(filters).then(setData);
  }, [filters]);  // filters 每次都變,導(dǎo)致無限循環(huán)

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

問題filters 每次渲染都是新對象,導(dǎo)致 effect 無限執(zhí)行。

解決方案 1:提取到組件外

const DEFAULT_FILTERS = { status: 'active', type: 'user' };

function DataList() {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetchData(DEFAULT_FILTERS).then(setData);
  }, []);  // 依賴空數(shù)組

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

解決方案 2:useMemo

function DataList() {
  const [data, setData] = useState([]);
  const filters = useMemo(() => ({ status: 'active', type: 'user' }), []);

  useEffect(() => {
    fetchData(filters).then(setData);
  }, [filters]);

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

解決方案 3:依賴具體的值

function DataList() {
  const [data, setData] = useState([]);
  const status = 'active';
  const type = 'user';

  useEffect(() => {
    fetchData({ status, type }).then(setData);
  }, [status, type]);  // 依賴原始值,不依賴對象

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

3.3 函數(shù)依賴導(dǎo)致的問題

function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const handleSearch = () => {
    fetchResults(query).then(setResults);
  };

  useEffect(() => {
    handleSearch();
  }, [handleSearch]);  // handleSearch 每次渲染都是新函數(shù),導(dǎo)致無限循環(huán)

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

解決方案 1:useCallback

const handleSearch = useCallback(() => {
  fetchResults(query).then(setResults);
}, [query]);

useEffect(() => {
  handleSearch();
}, [handleSearch]);

解決方案 2:直接在 effect 中調(diào)用

useEffect(() => {
  fetchResults(query).then(setResults);
}, [query]);  // 不依賴函數(shù),直接依賴 query

解決方案 3:useEffectEvent(React 19)

import { useEffectEvent } from 'react';

function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const handleSearch = useEffectEvent(() => {
    fetchResults(query).then(setResults);
  });

  useEffect(() => {
    handleSearch();  // 不需要添加到依賴數(shù)組
  }, []);

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

四、useCallback 和 useMemo 的誤用

4.1 過度使用導(dǎo)致的性能下降

function TodoList({ todos }) {
  // ? 過度優(yōu)化:簡單組件不需要 memo
  const renderItem = useCallback((todo) => {
    return <li key={todo.id}>{todo.title}</li>;
  }, []);

  // ? 過度優(yōu)化:簡單計(jì)算不需要 useMemo
  const count = useMemo(() => todos.length, [todos]);

  // ? 過度優(yōu)化:每個(gè)函數(shù)都 useCallback
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <div>
      <p>Total: {count}</p>
      <ul>{todos.map(renderItem)}</ul>
      <button onClick={handleClick}>Log</button>
    </div>
  );
}

問題

  • useCallbackuseMemo 本身有成本(創(chuàng)建閉包、比較依賴)
  • 簡單計(jì)算的成本可能低于 useMemo 的成本
  • 過度使用反而降低性能

原則

  • 只在真正需要時(shí)使用(子組件用了 React.memo、計(jì)算成本高)
  • 先測量,再優(yōu)化
  • 簡單組件不需要優(yōu)化

正確示例

function TodoList({ todos }) {
  // ? 簡單計(jì)算,不需要 useMemo
  const count = todos.length;

  // ? 子組件沒用 memo,不需要 useCallback
  const handleClick = () => {
    console.log('clicked');
  };

  return (
    <div>
      <p>Total: {count}</p>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
      <button onClick={handleClick}>Log</button>
    </div>
  );
}

4.2 useCallback 的依賴陷阱

function SearchBox() {
  const [query, setQuery] = useState('');
  const [filters, setFilters] = useState({ status: 'all' });

  // ? 缺少 filters 依賴
  const handleSearch = useCallback(() => {
    fetchResults(query, filters).then(setResults);
  }, [query]);  // filters 變化時(shí)不會(huì)更新

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}

解決

const handleSearch = useCallback(() => {
  fetchResults(query, filters).then(setResults);
}, [query, filters]);  // 添加所有依賴

但這又導(dǎo)致新問題:filters 是對象,每次渲染都變,useCallback 失效。

更好的解決方案

function SearchBox() {
  const [query, setQuery] = useState('');
  const [status, setStatus] = useState('all');  // 用原始值代替對象

  const handleSearch = useCallback(() => {
    fetchResults(query, { status }).then(setResults);
  }, [query, status]);  // 依賴原始值

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <select value={status} onChange={e => setStatus(e.target.value)}>
        <option value="all">All</option>
        <option value="active">Active</option>
      </select>
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}

4.3 useMemo 的計(jì)算時(shí)機(jī)

function ExpensiveComponent({ data }) {
  console.log('Component rendered');

  const result = useMemo(() => {
    console.log('Computing result');
    return data.map(item => item.value * 2);
  }, [data]);

  return <div>{result.join(', ')}</div>;
}

誤解:很多人以為 useMemo 會(huì)阻止組件重渲染。

真相

  • useMemo 只緩存計(jì)算結(jié)果,不阻止組件渲染
  • 組件每次渲染都會(huì)執(zhí)行,只是跳過 useMemo 內(nèi)部的計(jì)算
  • 要阻止組件渲染,需要用 React.memo

正確理解

// 組件每次都渲染,但計(jì)算只在 data 變化時(shí)執(zhí)行
function ExpensiveComponent({ data }) {
  console.log('Component rendered');  // 每次都打印

  const result = useMemo(() => {
    console.log('Computing result');  // 只在 data 變化時(shí)打印
    return data.map(item => item.value * 2);
  }, [data]);

  return <div>{result.join(', ')}</div>;
}

// 要阻止組件渲染,需要 React.memo
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
  console.log('Component rendered');  // 只在 data 變化時(shí)打印

  const result = useMemo(() => {
    console.log('Computing result');
    return data.map(item => item.value * 2);
  }, [data]);

  return <div>{result.join(', ')}</div>;
});

五、useRef 的常見誤區(qū)

5.1 useRef 不會(huì)觸發(fā)重渲染

function Counter() {
  const countRef = useRef(0);

  const handleClick = () => {
    countRef.current += 1;
    console.log('Count:', countRef.current);  // 打印正確的值
  };

  return (
    <div>
      <p>Count: {countRef.current}</p>  {/* 頁面不更新 */}
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

問題countRef.current 變化不會(huì)觸發(fā)重渲染,頁面顯示的值不變。

原因useRef 返回的是一個(gè)可變對象,修改 .current 不會(huì)觸發(fā)重渲染。

何時(shí)使用 useRef

  • 存儲(chǔ)不需要觸發(fā)渲染的值(定時(shí)器 ID、DOM 引用、上一次的值)
  • 在多次渲染間保持引用穩(wěn)定

何時(shí)使用 useState

  • 存儲(chǔ)需要觸發(fā)渲染的值

5.2 useRef 的初始化陷阱

function VideoPlayer({ src }) {
  const videoRef = useRef(null);

  useEffect(() => {
    // ? 錯(cuò)誤:videoRef.current 可能還是 null
    videoRef.current.play();
  }, []);

  return <video ref={videoRef} src={src} />;
}

問題useEffect 執(zhí)行時(shí),videoRef.current 可能還沒有賦值。

解決

function VideoPlayer({ src }) {
  const videoRef = useRef(null);

  useEffect(() => {
    // ? 檢查是否已賦值
    if (videoRef.current) {
      videoRef.current.play();
    }
  }, []);

  return <video ref={videoRef} src={src} />;
}

或者用回調(diào) ref:

function VideoPlayer({ src }) {
  const handleRef = useCallback((node) => {
    if (node) {
      node.play();
    }
  }, []);

  return <video ref={handleRef} src={src} />;
}

5.3 useRef 存儲(chǔ)上一次的值

這是 useRef 的經(jīng)典用法:

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>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

六、自定義 Hook 的陷阱

6.1 忘記返回清理函數(shù)

// ? 錯(cuò)誤:沒有清理定時(shí)器
function useInterval(callback, delay) {
  useEffect(() => {
    const timer = setInterval(callback, delay);
    // 忘記返回清理函數(shù)
  }, [callback, delay]);
}

問題:組件卸載時(shí)定時(shí)器沒有清除,導(dǎo)致內(nèi)存泄漏。

解決

function useInterval(callback, delay) {
  useEffect(() => {
    const timer = setInterval(callback, delay);
    return () => clearInterval(timer);  // 清理函數(shù)
  }, [callback, delay]);
}

6.2 依賴不穩(wěn)定導(dǎo)致的問題

// ? 錯(cuò)誤:callback 每次渲染都變
function useInterval(callback, delay) {
  useEffect(() => {
    const timer = setInterval(callback, delay);
    return () => clearInterval(timer);
  }, [callback, delay]);  // callback 變化導(dǎo)致定時(shí)器重啟
}

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

  useInterval(() => {
    console.log('Count:', count);  // 每次 count 變化,定時(shí)器都重啟
  }, 1000);

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

解決方案 1:useRef 存儲(chǔ) callback

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

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

  useEffect(() => {
    const timer = setInterval(() => {
      savedCallback.current();
    }, delay);
    return () => clearInterval(timer);
  }, [delay]);  // 只依賴 delay
}

解決方案 2:useEffectEvent(React 19)

import { useEffectEvent } from 'react';

function useInterval(callback, delay) {
  const onTick = useEffectEvent(callback);

  useEffect(() => {
    const timer = setInterval(onTick, delay);
    return () => clearInterval(timer);
  }, [delay]);
}

6.3 條件調(diào)用 Hook

function useUser(userId) {
  if (!userId) {
    return null;  // ? 錯(cuò)誤:條件返回
  }

  const [user, setUser] = useState(null);  // Hook 在條件語句后

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  return user;
}

問題:違反了 Hooks 規(guī)則(Hooks 必須在頂層調(diào)用)。

解決

function useUser(userId) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (!userId) {
      setUser(null);
      return;
    }

    fetchUser(userId).then(setUser);
  }, [userId]);

  return user;
}

七、內(nèi)存泄漏的常見場景

7.1 異步操作未取消

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

  useEffect(() => {
    fetchUser(userId).then(user => {
      setUser(user);  // 組件卸載后仍然執(zhí)行,導(dǎo)致警告
    });
  }, [userId]);

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

問題:組件卸載后,fetchUser 的回調(diào)仍然執(zhí)行,嘗試更新已卸載組件的狀態(tài)。

解決方案 1:使用標(biāo)志位

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

  useEffect(() => {
    let cancelled = false;

    fetchUser(userId).then(user => {
      if (!cancelled) {
        setUser(user);
      }
    });

    return () => {
      cancelled = true;
    };
  }, [userId]);

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

解決方案 2:使用 AbortController

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

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(res => res.json())
      .then(setUser)
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err);
        }
      });

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

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

7.2 事件監(jiān)聽器未移除

function WindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

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

    window.addEventListener('resize', handleResize);
    // ? 忘記移除監(jiān)聽器
  }, []);

  return <div>{size.width} x {size.height}</div>;
}

解決

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

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

7.3 定時(shí)器未清除

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

  useEffect(() => {
    setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    // ? 忘記清除定時(shí)器
  }, []);

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

問題:組件卸載后定時(shí)器仍在運(yùn)行。

解決

useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);

  return () => clearInterval(timer);  // 清理
}, []);

八、useContext 的性能陷阱

8.1 Context 導(dǎo)致的全局重渲染

const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [settings, setSettings] = useState({});

  const value = { user, setUser, theme, setTheme, settings, setSettings };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

function UserName() {
  const { user } = useContext(AppContext);
  return <div>{user?.name}</div>;
}

function ThemeToggle() {
  const { theme, setTheme } = useContext(AppContext);
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}

問題theme 變化時(shí),UserName 組件也會(huì)重渲染(雖然它只用到 user)。

解決方案 1:拆分 Context

const UserContext = createContext();
const ThemeContext = createContext();

function UserName() {
  const { user } = useContext(UserContext);  // 只訂閱 user
  return <div>{user?.name}</div>;
}

function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);  // 只訂閱 theme
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}

解決方案 2:使用 useMemo 穩(wěn)定 value

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  const value = useMemo(() => ({
    user, setUser, theme, setTheme
  }), [user, theme]);

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

解決方案 3:使用狀態(tài)管理庫

// 使用 Zustand
import create from 'zustand';

const useStore = create((set) => ({
  user: null,
  theme: 'light',
  setUser: (user) => set({ user }),
  setTheme: (theme) => set({ theme })
}));

function UserName() {
  const user = useStore(state => state.user);  // 只訂閱 user
  return <div>{user?.name}</div>;
}

function ThemeToggle() {
  const theme = useStore(state => state.theme);  // 只訂閱 theme
  const setTheme = useStore(state => state.setTheme);
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}

九、useReducer 的常見問題

9.1 reducer 中的副作用

function reducer(state, action) {
  switch (action.type) {
    case 'fetch_success':
      // ? 錯(cuò)誤:在 reducer 中執(zhí)行副作用
      localStorage.setItem('data', JSON.stringify(action.payload));
      return { ...state, data: action.payload };
    default:
      return state;
  }
}

問題:reducer 應(yīng)該是純函數(shù),不應(yīng)該有副作用。

解決:副作用放在 useEffect

function reducer(state, action) {
  switch (action.type) {
    case 'fetch_success':
      return { ...state, data: action.payload };
    default:
      return state;
  }
}

function Component() {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    if (state.data) {
      localStorage.setItem('data', JSON.stringify(state.data));
    }
  }, [state.data]);
}

9.2 dispatch 的穩(wěn)定性

function Parent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return <Child onUpdate={dispatch} />;  // dispatch 是穩(wěn)定的,不需要 useCallback
}

const Child = React.memo(function Child({ onUpdate }) {
  return <button onClick={() => onUpdate({ type: 'increment' })}>+1</button>;
});

好消息dispatch 函數(shù)是穩(wěn)定的,不會(huì)在重渲染時(shí)改變,不需要用 useCallback 包裹。

十、調(diào)試 Hooks 的工具與技巧

10.1 React DevTools

查看 Hooks 狀態(tài)

  1. 打開 React DevTools
  2. 選擇組件
  3. 右側(cè)面板顯示所有 Hooks 的值

查看重渲染原因

  1. 打開 Profiler
  2. 錄制操作
  3. 查看「Why did this render」

10.2 自定義調(diào)試 Hook

function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();

  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changedProps = {};

      allKeys.forEach(key => {
        if (previousProps.current[key] !== props[key]) {
          changedProps[key] = {
            from: previousProps.current[key],
            to: props[key]
          };
        }
      });

      if (Object.keys(changedProps).length > 0) {
        console.log('[why-did-you-update]', name, changedProps);
      }
    }

    previousProps.current = props;
  });
}

// 使用
function MyComponent(props) {
  useWhyDidYouUpdate('MyComponent', props);
  return <div>...</div>;
}

10.3 ESLint 插件

安裝 eslint-plugin-react-hooks

npm install eslint-plugin-react-hooks --save-dev

配置 .eslintrc.js

module.exports = {
  plugins: ['react-hooks'],
  rules: {
    'react-hooks/rules-of-hooks': 'error',  // 檢查 Hooks 規(guī)則
    'react-hooks/exhaustive-deps': 'warn'   // 檢查依賴數(shù)組
  }
};

會(huì)自動(dòng)檢測:

  • Hooks 是否在頂層調(diào)用
  • 依賴數(shù)組是否完整
  • 是否在條件語句中調(diào)用 Hooks

十一、Hooks 最佳實(shí)踐總結(jié)

11.1 useState

推薦

  • 用函數(shù)式更新避免閉包陷阱
  • 相關(guān)的狀態(tài)合并成一個(gè)對象
  • 簡單狀態(tài)用 useState,復(fù)雜狀態(tài)用 useReducer

避免

  • 在異步回調(diào)中直接使用 state
  • 過度拆分狀態(tài)(導(dǎo)致多次渲染)
  • 用 state 存儲(chǔ)可計(jì)算的值

11.2 useEffect

推薦

  • 依賴數(shù)組包含所有使用的外部變量
  • 返回清理函數(shù)
  • 一個(gè) effect 只做一件事

避免

  • 空依賴數(shù)組 + 使用外部變量
  • 對象/數(shù)組作為依賴
  • 在 effect 中修改依賴的值(導(dǎo)致無限循環(huán))

11.3 useCallback / useMemo

推薦

  • 子組件用了 React.memo 時(shí)使用
  • 計(jì)算成本高時(shí)使用
  • 先測量,再優(yōu)化

避免

  • 過度使用(反而降低性能)
  • 依賴數(shù)組不完整
  • 用于簡單計(jì)算

11.4 useRef

推薦

  • 存儲(chǔ)不需要觸發(fā)渲染的值
  • 存儲(chǔ) DOM 引用
  • 存儲(chǔ)上一次的值

避免

  • 用 ref 存儲(chǔ)需要觸發(fā)渲染的值
  • 在渲染期間修改 ref.current

11.5 自定義 Hook

推薦

  • 提取可復(fù)用的邏輯
  • 返回清理函數(shù)
  • 使用 use 前綴命名

避免

  • 條件調(diào)用 Hook
  • 忘記清理副作用
  • 依賴不穩(wěn)定

十二、總結(jié)

React Hooks 的常見陷阱:

  • 閉包陷阱:異步操作中使用 state,用函數(shù)式更新解決
  • 依賴地獄:對象/數(shù)組依賴導(dǎo)致無限循環(huán),用 useMemo 或原始值解決
  • 過度優(yōu)化:不要給所有東西都加 useCallback/useMemo
  • 內(nèi)存泄漏:忘記清理定時(shí)器、事件監(jiān)聽器、異步操作
  • Context 性能:拆分 Context 或使用狀態(tài)管理庫
  • useRef 誤用:記住它不會(huì)觸發(fā)重渲染

避坑指南:

  • 使用 ESLint 插件自動(dòng)檢查
  • 用 React DevTools 調(diào)試
  • 先測量,再優(yōu)化
  • 遵循 Hooks 規(guī)則
  • 寫清理函數(shù)
  • 依賴數(shù)組要完整

記住:Hooks 很強(qiáng)大,但也很容易踩坑。理解原理比記住規(guī)則更重要。

附錄:常用自定義 Hooks

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

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

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

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

  return [value, setValue];
}

// 3. useOnClickOutside
function useOnClickOutside(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]);
}

// 4. useAsync
function useAsync(asyncFunction, immediate = true) {
  const [status, setStatus] = useState('idle');
  const [value, setValue] = useState(null);
  const [error, setError] = useState(null);

  const execute = useCallback(() => {
    setStatus('pending');
    setValue(null);
    setError(null);

    return asyncFunction()
      .then(response => {
        setValue(response);
        setStatus('success');
      })
      .catch(error => {
        setError(error);
        setStatus('error');
      });
  }, [asyncFunction]);

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

  return { execute, status, value, error };
}

以上就是從閉包陷阱到內(nèi)存泄漏解析React Hooks避坑指南的詳細(xì)內(nèi)容,更多關(guān)于React Hooks避坑的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • React + Recharts 圖表點(diǎn)擊時(shí)出現(xiàn)黑色邊框的問題及解決方法

    React + Recharts 圖表點(diǎn)擊時(shí)出現(xiàn)黑色邊框的問題及解決方法

    本文給大家介紹React + Recharts 圖表點(diǎn)擊時(shí)出現(xiàn)黑色邊框的問題及解決方法,這是由于瀏覽器的默認(rèn)focusoutline樣式,嘗試了多種解決方案,感興趣的朋友跟隨小編一起看看吧
    2025-12-12
  • vite?+?react?+typescript?環(huán)境搭建小白入門教程

    vite?+?react?+typescript?環(huán)境搭建小白入門教程

    這篇文章主要介紹了vite?+?react?+typescript?環(huán)境搭建小白入門教程,本文通過示例圖文相結(jié)合給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-12-12
  • 搭建React?Native熱更新平臺(tái)的詳細(xì)過程

    搭建React?Native熱更新平臺(tái)的詳細(xì)過程

    這篇文章主要介紹了搭建React?Native熱更新平臺(tái),要實(shí)現(xiàn)?React?Native?熱更新功能,有四種思路?Code?Push、Pushy、Expo?和自研,感興趣的朋友一起通過本文學(xué)習(xí)吧
    2022-05-05
  • react路由基礎(chǔ)解讀(Router、Link和Route)

    react路由基礎(chǔ)解讀(Router、Link和Route)

    這篇文章主要介紹了react路由基礎(chǔ)解讀(Router、Link和Route),具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • React結(jié)合Drag?API實(shí)現(xiàn)拖拽示例詳解

    React結(jié)合Drag?API實(shí)現(xiàn)拖拽示例詳解

    這篇文章主要為大家介紹了React結(jié)合Drag?API實(shí)現(xiàn)拖拽示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-03-03
  • React實(shí)現(xiàn)路由返回?cái)r截的三種方式

    React實(shí)現(xiàn)路由返回?cái)r截的三種方式

    最近項(xiàng)目為了避免用戶誤操作導(dǎo)致數(shù)據(jù)丟失,增加返回?cái)r截功能,但是之前由于qiankun的報(bào)錯(cuò)導(dǎo)致這個(gè)功能一直有一些問題,所以專門獨(dú)立搞了一個(gè)專題研究在react中各種方式實(shí)現(xiàn)這個(gè)功能,需要的朋友可以參考下
    2024-05-05
  • react框架next.js學(xué)習(xí)之API?路由篇詳解

    react框架next.js學(xué)習(xí)之API?路由篇詳解

    這篇文章主要為大家介紹了react框架next.js學(xué)習(xí)之API?路由篇詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • React路由規(guī)則定義與聲明式導(dǎo)航及編程式導(dǎo)航分別介紹

    React路由規(guī)則定義與聲明式導(dǎo)航及編程式導(dǎo)航分別介紹

    這篇文章主要介紹了React路由規(guī)則的定義、聲明式導(dǎo)航、編程式導(dǎo)航,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧
    2022-09-09
  • react中實(shí)現(xiàn)將一個(gè)視頻流為m3u8格式的轉(zhuǎn)換

    react中實(shí)現(xiàn)將一個(gè)視頻流為m3u8格式的轉(zhuǎn)換

    這篇文章主要介紹了react中實(shí)現(xiàn)將一個(gè)視頻流為m3u8格式的轉(zhuǎn)換方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • react-router v4如何使用history控制路由跳轉(zhuǎn)詳解

    react-router v4如何使用history控制路由跳轉(zhuǎn)詳解

    這篇文章主要給大家介紹了關(guān)于react-router v4如何使用history控制路由跳轉(zhuǎn)的相關(guān)資料,文中通過示例代碼介紹的的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2018-01-01

最新評(píng)論

林周县| 潜江市| 张北县| 岗巴县| 旌德县| 乌恰县| 嵩明县| 乌海市| 巴东县| 静宁县| 天门市| 谢通门县| 如东县| 湟中县| 临澧县| 都安| 东方市| 东丰县| 铅山县| 老河口市| 丰宁| 鹤山市| 曲阜市| 南开区| 阿城市| 蓝田县| 洛隆县| 筠连县| 关岭| 青龙| 轮台县| 米林县| 那坡县| 洛浦县| 珲春市| 墨脱县| 重庆市| 新龙县| 白水县| 湟中县| 衡东县|