我有一个使用Hooks的功能组件:
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>
);
}
该组件会在安装时获取一些items
并将其保存到状态。
该组件从React Router接收一个itemId
道具。
只要props.itemId
发生变化,我都希望它触发一个效果,在这种情况下,将其记录到控制台。
问题在于,由于效果还取决于items
,因此只要items
发生变化,效果也将运行,例如,通过按按钮。
可以通过将前一个items
存储在一个单独的状态变量中并进行比较来解决此问题,但这似乎很简单,并且增加了样板。通过使用组件类,可以通过比较props.itemId
中的当前和以前的道具来解决,但是使用功能组件是不可能的,这是使用Hooks的要求。
仅当其中一个参数发生更改时,触发取决于多个参数的效果的最佳方法是什么?
PS。挂钩是一种新事物,我认为我们都在尽力找出如何正确使用挂钩的功能,因此,如果我的想法对您来说似乎是错误的或尴尬的,请指出。
答案 0 :(得分:3)
最近在一个项目中遇到了这个问题,我们的解决方案是将useEffect
的内容移动到回调(在这种情况下是已记忆)中,并调整两者的依赖关系。使用您提供的代码,它看起来像这样:
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>
);
}
因此useEffect
仅以ID属性作为其依赖项,并回调ID的项目和。
实际上,您可以从回调中删除ID依赖项,并将其作为参数传递给onItemIdChange
回调:
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 ])
答案 1 :(得分:1)
React小组说,获取prev值的最佳方法是使用useRef:https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
function Component(props) {
const [ items, setItems ] = useState([]);
const prevItemIdRef = useRef();
useEffect(() => {
prevItemIdRef.current = props.itemId;
});
const prevItemId = prevItemIdRef.current;
// 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(prevItemId !== props.itemId) {
console.log('diff itemId');
}
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>
);
}
我认为这对您有帮助。
注意:如果不需要以前的值,另一种方法是为props.itemId写一个useEffect more
React.useEffect(() => {
console.log('track changes for itemId');
}, [props.itemId]);
答案 2 :(得分:1)
一种简单的方法是编写一个自定义钩子来帮助我们
// Desired hook
const useCompare = (val) => {
const prevVal = usePrevious(val)
return prevVal !== val
}
// Helper hook
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
然后在useEffect
const Component = (props) => {
const hasItemIdChanged = useCompare(props.itemId);
useEffect(() => {
if(hasItemIdChanged) {
// ...
}
}, [items, props.itemId])
}
答案 3 :(得分:0)
在提供的示例中,您的效果不取决于items
和itemId
,而是取决于集合中的一项。
是的,您需要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) ])
答案 4 :(得分:0)
我只是自己尝试过一次,在我看来,您不需要将它们放入useEffect
依赖项列表中就可以拥有更新的版本。意味着您可以只放入props.itemId
,仍然在效果中使用items
。
我在这里创建了一个片段来尝试证明/说明这一点。让我知道是否有问题。
const Child = React.memo(props => {
const [items, setItems] = React.useState([]);
const fetchItems = () => {
setTimeout(() => {
setItems((old) => {
const newItems = [];
for (let i = 0; i < old.length + 1; i++) {
newItems.push(i);
}
return newItems;
})
}, 1000);
}
React.useEffect(() => {
console.log('OLD (logs on both buttons) id:', props.id, 'items:', items.length);
}, [props.id, items]);
React.useEffect(() => {
console.log('NEW (logs on only the red button) id:', props.id, 'items:', items.length);
}, [props.id]);
return (
<div
onClick={fetchItems}
style={{
width: "200px",
height: "100px",
marginTop: "12px",
backgroundColor: 'orange',
textAlign: "center"
}}
>
Click me to add a new item!
</div>
);
});
const Example = () => {
const [id, setId] = React.useState(0);
const updateId = React.useCallback(() => {
setId(old => old + 1);
}, []);
return (
<div style={{ display: "flex", flexDirection: "row" }}>
<Child
id={id}
/>
<div
onClick={updateId}
style={{
width: "200px",
height: "100px",
marginTop: "12px",
backgroundColor: 'red',
textAlign: "center"
}}
>Click me to update the id</div>
</div>
);
};
ReactDOM.render(<Example />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
答案 5 :(得分:0)
我是React hooks的初学者,所以可能不对,但是我最终为这种情况定义了一个自定义钩子。
query GetMyModel(id: ID!){
getMyModel(id: $id){
someProperty
}
}
它监视第二个依赖项数组(可以小于useEffect依赖项)以查找更改,并在这些更改中的任何一个产生原始的useEffect。
以下是在示例中如何使用(和重用)它而不是useEffect的方法:
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
);
}
Here's a simplified example of it in action,useEffectWhen仅在id更改时显示在控制台中,而useEffect则在项目或ID更改时记录日志。
这将在没有任何附带警告的情况下起作用,但这主要是因为它混淆了详尽说明的附带规则!如果要确保您拥有所需的部门,可以在eslint规则中包括useEffectWhen。您将在package.json中使用它:
// 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])
,也可以选择在您的.env文件中将此文件供react-scripts提取:
"eslintConfig": {
"extends": "react-app",
"rules": {
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "useEffectWhen"
}
]
}
},