Javascript 当只有一个效果';s deps发生了变化,但其他的没有变化

Javascript 当只有一个效果';s deps发生了变化,但其他的没有变化,javascript,reactjs,react-hooks,Javascript,Reactjs,React Hooks,我有一个使用钩子的功能组件: function Component(props) { const [ items, setItems ] = useState([]); // In a callback Hook to prevent unnecessary re-renders const handleFetchItems = useCallback(() => { fetchItemsFromApi().then(setItems); }, []);

我有一个使用钩子的功能组件:

function Component(props) {
  const [ items, setItems ] = useState([]);

  // In a callback Hook to prevent unnecessary re-renders 
  const handleFetchItems = useCallback(() => {
    fetchItemsFromApi().then(setItems);
  }, []);

  // Fetch items on mount
  useEffect(() => {
    handleFetchItems();
  }, []);

  // I want this effect to run only when 'props.itemId' changes,
  // not when 'items' changes
  useEffect(() => {
    if (items) {
      const item = items.find(item => item.id === props.itemId);
      console.log("Item changed to " item.name);
    }
  }, [ items, props.itemId ])

  // Clicking the button should NOT log anything to console
  return (
    <Button onClick={handleFetchItems}>Fetch items</Button>
  );
}
功能组件(道具){
const[items,setItems]=useState([]);
//在回调挂钩中,以防止不必要的重新渲染
const handleFetchItems=useCallback(()=>{
fetchItemsFromApi()。然后(setItems);
}, []);
//取挂载物品
useffect(()=>{
handleFetchItems();
}, []);
//我希望仅当“props.itemId”更改时才运行此效果,
//当“项目”发生变化时不会发生
useffect(()=>{
若有(项目){
const item=items.find(item=>item.id==props.itemId);
console.log(“项更改为”项.name);
}
},[items,props.itemId])
//单击按钮不应将任何内容记录到控制台
返回(
取货
);
}
组件在装载时获取一些
,并将它们保存到状态

组件接收一个
itemId
prop(来自React路由器)

每当
props.itemId
更改时,我希望它触发一个效果,在本例中,将其记录到控制台


问题在于,由于效果也取决于
,因此每当
发生变化时,例如当按下按钮重新获取
时,效果也将运行

这可以通过将前面的
props.itemId
存储在一个单独的状态变量中并对两者进行比较来解决,但这似乎是一个黑客攻击,并添加了样板文件。使用组件类可以通过比较
componentdiddupdate
中当前和以前的道具来解决这个问题,但是使用功能组件是不可能的,这是使用钩子的一个要求


仅当其中一个参数发生变化时,触发依赖于多个参数的效果的最佳方法是什么?



PS.Hooks是一种新事物,我认为我们都在尽最大努力找出如何正确使用它们,因此,如果我的想法对您来说是错误或尴尬的,请指出。

React团队说,获取prev值的最佳方法是使用useRef:


从提供的示例中,您的效果并不取决于
项ID
,而是集合中的一个项

是的,您需要
items
itemId
来获取该项,但这并不意味着您必须在依赖项数组中指定它们

要确保仅在目标项更改时执行该项,应使用相同的查找逻辑将该项传递给依赖项数组

useEffect(() => {
  if (items) {
    const item = items.find(item => item.id === props.itemId);
    console.log("Item changed to " item.name);
  }
}, [ items.find(item => item.id === props.itemId) ])

我自己就试过了,在我看来,你不需要把东西放在
useffect
依赖项列表中,就可以得到它们的更新版本。这意味着您只需放入
props.itemId
,并且仍然可以在效果中使用
items

我在这里创建了一个片段,试图证明/说明这一点。如果有什么不对劲,请告诉我

const Child=React.memo(props=>{
const[items,setItems]=React.useState([]);
常量fetchItems=()=>{
设置超时(()=>{
设置项((旧)=>{
常量newItems=[];
for(设i=0;i{
console.log('OLD(两个按钮上的日志)id:',props.id',items:',items.length);
},[props.id,items]);
React.useffect(()=>{
console.log('NEW(仅在红色按钮上登录)id:',props.id',items:',items.length);
},[props.id]);
返回(
单击我添加新项目!
);
});
常量示例=()=>{
const[id,setId]=React.useState(0);
const updateId=React.useCallback(()=>{
setId(old=>old+1);
}, []);
返回(
单击我更新id
);
};
render(,document.getElementById(“根”))

我是react hooks的初学者,所以这可能不对,但我最终为此类场景定义了一个自定义挂钩:

const useEffectWhen = (effect, deps, whenDeps) => {
  const whenRef = useRef(whenDeps || []);
  const initial = whenRef.current === whenDeps;
  const whenDepsChanged = initial || !whenRef.current.every((w, i) => w === whenDeps[i]);
  whenRef.current = whenDeps;
  const nullDeps = deps.map(() => null);

  return useEffect(
    whenDepsChanged ? effect : () => {},
    whenDepsChanged ? deps : nullDeps
  );
}
它监视第二个依赖项数组(可以小于useffect依赖项)的更改&如果这些更改中有任何更改,则生成原始useffect

以下是如何在示例中使用(并重用)它,而不是使用效果:

// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffectWhen(() => {
  if (items) {
    const item = items.find(item => item.id === props.itemId);
    console.log("Item changed to " item.name);
  }
}, [ items, props.itemId ], [props.itemId])
,useEffectWhen将仅在id更改时显示在控制台中,而useEffectWhen则在项目或id更改时记录

这将在没有任何eslint警告的情况下工作,但这主要是因为它混淆了eslint规则的穷举DEP!如果要确保拥有所需的DEP,可以将useEffectWhen包括在eslint规则中。您需要在您的package.json中包含以下内容:

"eslintConfig": {
  "extends": "react-app",
  "rules": {
    "react-hooks/exhaustive-deps": [
      "warn",
      {
        "additionalHooks": "useEffectWhen"
      }
    ]
  }
},
并且可以在.env文件中选择此选项,以便react脚本拾取它:

EXTEND_ESLINT=true
⚠️ 注意:此答案目前不正确,可能导致意外的错误/副作用。
useCallback
变量必须是
useffect
钩子的依赖项,因此会导致与OP面临的问题相同的问题

我会尽快处理

最近在一个项目中遇到了这种情况,我们的解决方案是将
useffect
的内容移动到回调中(在本例中被记录),并调整两者的依赖关系。根据您提供的代码,它看起来如下所示:

function Component(props) {
  const [ items, setItems ] = useState([]);

  const onItemIdChange = useCallback(() => {
    if (items) {
      const item = items.find(item => item.id === props.itemId);
      console.log("Item changed to " item.name);
    }
  }, [items, props.itemId]);

  // I want this effect to run only when 'props.itemId' changes,
  // not when 'items' changes
  useEffect(onItemIdChange, [ props.itemId ]);

  // Clicking the button should NOT log anything to console
  return (
    <Button onClick={handleFetchItems}>Fetch items</Button>
  );
}

一个简单的解决方法是编写一个自定义钩子来帮助我们实现这一点

// Desired hook
function useCompare (val) {
  const prevVal = usePrevious(val)
  return prevVal !== val
}

// Helper hook
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}
然后在
useffect

function Component(props) {
  const hasItemIdChanged = useCompare(props.itemId);
  useEffect(() => {
    if(hasItemIdChanged) {
      // …
    }
  }, [hasItemIdChanged])
  return <></>
}
功能组件(道具){
const hasItemIdChanged=useCompare(props.itemId);
useffect(()=>{
如果(hasItemIdChanged){
// …
}
},[hasItemIdChanged])
返回
}

我面临着完全相同的问题。。。说得好!使用refs的解决方案很麻烦,但现在它是唯一的解决方案!然而,要区分“whatDeps”和“whenDeps”,这正好满足您的需求。请随意评论/上传
const onItemIdChange = useCallback((id) => {
  if (items) {
    const item = items.find(item => item.id === id);
    console.log("Item changed to " item.name);
  }
}, [items]);

useEffect(() => {
  onItemIdChange(props.itemId)
}, [ props.itemId ]) 
// Desired hook
function useCompare (val) {
  const prevVal = usePrevious(val)
  return prevVal !== val
}

// Helper hook
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}
function Component(props) {
  const hasItemIdChanged = useCompare(props.itemId);
  useEffect(() => {
    if(hasItemIdChanged) {
      // …
    }
  }, [hasItemIdChanged])
  return <></>
}