Skip to content

React闭包陷阱

原文转载地址:

react的闭包陷阱

前几天面试被问到react中的闭包陷阱,被面试官一顿吊打,特此记录下来

React 的闭包陷阱是指在使用 React Hooks 时,由于闭包特性导致在某些函数或异步操作中无法正确访问到更新后状态或 prop 的值,而仍旧使用了旧值。下面通过几个代码示例来具体说明闭包陷阱的几种常见情形:

useState闭包陷阱

第一个例子

jsx
/**
 * useState闭包陷阱
 */

import React from 'react'
import { useState } from 'react'

function App01() {

    const [count, setCount] = useState(0)

    const handleClick = () => {
        setTimeout(() => {
            console.log('Count inside setTimeout:', count); // 这里打印的一直是上一个旧值,形成了闭包陷阱
        }, 1000)
        setCount(count + 1)
    }

    return (
        <div>
            <p>Current Count:{count}</p>
            <button onClick={() => { handleClick() }}>Increment</button>
        </div>
    )
}

export default App01

在这个例子中,handleClick 函数内的 setTimeout 回调形成了一个闭包,它捕获了首次渲染时 count 的值(即 0)。当用户点击按钮触发 handleClick 时,虽然 setCount 更新了状态,但 setTimeout 回调内的 count 仍指向最初捕获的 0,因此在延时一秒后打印出的 count 值不是预期的更新值

第二个例子

下面是一个关于react父子组件传值的闭包陷阱示例

Child.jsx

jsx
import React, {useState} from 'react'

function Child(props) {
    const [data, setData] = useState(0)

    const sendDataToFather = () => {
        setData(data + 1)
        props.onSharedDataChange(data)
    }

    return (
        <div>
            <div>子组件中的data: { data }</div>
            <button onClick={ () => { sendDataToFather() } }>Send Data To Father</button>
        </div>
    )
}

export default Child

Parent.jsx

jsx
import React, {useState} from 'react'
import Child from "./Child";

function Parent() {
    const [shareData, setShareData] = useState(0)

    const onSharedDataChange = (data) => {
        setShareData(data)
    }

    return (
        <div>
            <div>
                父组件中的data: {shareData}
            </div>
            <div>
                <Child onSharedDataChange={onSharedDataChange}/>
            </div>
        </div>
    )
}

export default Parent

预期结果应该是父组件和子组件中的data始终保持一致,但是可以打先父组件中的data始终是上一次更新的旧值

解决方法,使用useEffect监听依赖: Child.jsx

jsx
import React, { useState, useEffect } from 'react'

function Child(props) {
    const [data, setData] = useState(0)

    const sendDataToFather = () => {
        setData(data + 1)
        props.onSharedDataChange(data)
    }

    // 设置一个依赖收集的副作用函数,数据变化后重新通信传值
    useEffect(() => {
        props.onSharedDataChange(data)
    }, [data])

    return (
        <div>
            <div>子组件中的data: {data}</div>
            <button onClick={() => { sendDataToFather() }}>Send Data To Father</button>
        </div>
    )
}

export default Child

第三个例子

jsx
import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1); // 闭包陷阱:这里的 count 始终是初始值 0
    }, 1000);

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

  const handleClick = () => {
    console.log(`Current count: ${count}`);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Show Count</button>
    </div>
  );
}

export default Counter;

在这个示例中,setInterval 回调函数中的 count 始终是初始值 0,因为这个回调函数是在组件第一次渲染时创建的,因此它捕获了 count 的初始值。解决这个问题的方法是使用函数形式的 setState:

解决办法:

jsx
useEffect(() => {
  const intervalId = setInterval(() => {
    setCount(prevCount => prevCount + 1); // 使用函数形式的 setState
  }, 1000);

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

useEffect闭包陷阱

jsx
import { useState, useEffect } from 'react';
 
function FetchUser() {
  const [userId, setUserId] = useState(1);
  const [user, setUser] = useState(null);
 
  useEffect(() => {
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(data => setUser(data));
  }, []); // 问题:遗漏了 `userId` 作为依赖
 
  function handleUserIdChange(newId) {
    setUserId(newId);
  }
 
  return (
    <>
      <input type="number" value={userId} onChange={e => handleUserIdChange(e.target.value)} />
      {user && <p>User Name: {user.name}</p>}
    </>
  );
}

此处 useEffect 用于获取指定 userId 的用户信息。然而,useEffect 的依赖数组为空,意味着它仅在组件挂载时执行一次。当 handleUserIdChange 调用 setUserId 更新 userId 时,useEffect 不会重新执行,因为它没有将 userId 列为依赖。结果,尽管用户输入了新的 ID,fetch 请求仍使用了初始的 userId 值(即 1),导致界面展示的是错误的用户信息。

解决方法:

正确列出 useEffect 的依赖

jsx
useEffect(() => {
  // ...
}, [userId]);

useCallback闭包陷阱

jsx
import { useState, useCallback } from 'react';
 
function FilteredList({ items }) {
  const [filterText, setFilterText] = useState('');
 
  const filteredItems = items.filter(item => item.includes(filterText));
 
  const handleFilterChange = useCallback(
    event => {
      setFilterText(event.target.value);
    },
    [] // 问题:遗漏了 `setFilterText` 作为依赖
  );
 
  return (
    <div>
      <input type="text" value={filterText} onChange={handleFilterChange} />
      <ul>
        {filteredItems.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

这里 useCallback 用于缓存 handleFilterChange 函数以避免不必要的重渲染。然而,依赖数组为空,意味着 handleFilterChange 在组件整个生命周期内都不会改变。当 setFilterText 被外部因素(如热重载)替换时,handleFilterChange 仍然引用着旧的 setFilterText 实例,导致过滤功能失效。

解决方案:

setFilterText 添加到 useCallback 的依赖列表:

jsx
const handleFilterChange = useCallback(
  event => {
    setFilterText(event.target.value);
  },
  [setFilterText]
);

上次更新于: