给出项目列表(来自服务器):
const itemsFromServer = {
"1": {
id: "1",
value: "test"
},
"2": {
id: "2",
value: "another row"
}
};
我们要渲染每个项目,但仅在必要时进行某些更改:
const Item = React.memo(function Item({ id, value, onChange, onSave }) {
console.log("render", id);
return (
<li>
<input
value={value}
onChange={event => onChange(id, event.target.value)}
/>
<button onClick={() => onSave(id)}>Save</button>
</li>
);
});
handleSave
函数的ItemList函数组件,需要对其进行记忆。并且可以保存每个单独的项目:
function ItemList() {
const [items, setItems] = useState(itemsFromServer);
const handleChange = useCallback(
function handleChange(id, value) {
setItems(currentItems => {
return {
...currentItems,
[id]: {
...currentItems[id],
value
}
};
});
},
[setItems]
);
async function handleSave(id) {
const item = items[id];
if (item.value.length < 5) {
alert("Incorrect length.");
return;
}
await save(item);
alert("Save done :)");
}
return (
<ul>
{Object.values(items).map(item => (
<Item
key={item.id}
id={item.id}
value={item.value}
onChange={handleChange}
onSave={handleSave}
/>
))}
</ul>
);
}
当只有一项更改时,如何防止每个Item
不必要地重新渲染?
当前,在每个渲染上都会创建一个新的handleSave
函数。使用useCallback
时,items
对象包含在依赖项列表中。
将value
作为参数传递给handleSave
,从而从items
的依赖项列表中删除handleSave
对象。在此示例中,这可能是一个不错的解决方案,但是由于多种原因,在现实生活中它并不是首选(例如,更多的参数等)。
使用单独的组件ItemWrapper
,可以在其中记住handleSave
函数。
function ItemWrapper({ item, onChange, onSave }) {
const memoizedOnSave = useCallback(onSave, [item]);
return (
<Item
id={item.id}
value={item.value}
onChange={onChange}
onSave={memoizedOnSave}
/>
);
}
使用useRef()
钩子,在对items
的每次更改中,将其写入ref,并在items
函数内部从ref读取handleSave
。
在该状态下保留变量idToSave
。设置为保存。然后使用useEffect(() => { /* save */ }, [idToSave])
触发保存功能。 “被动地”。
以上所有解决方案对我而言似乎都不理想。 是否还有其他方法可以防止在每个渲染器上为每个handleSave
创建一个新的Item
函数,从而防止不必要的重新渲染?这样吗?
CodeSandbox:https://codesandbox.io/s/wonderful-tesla-9wcph?file=/src/App.js
答案 0 :(得分:0)
我要问的第一个问题:重新渲染真的有问题吗?
您是对的,react将为您在此处具有的每个函数重新调用每个渲染,但是您的DOM不应有太大改变,这可能没什么大不了的。
如果在渲染Item
时进行了大量计算,则可以记住繁琐的计算。
如果您真的想优化此代码,我会在这里看到不同的解决方案:
ItemList
更改为类组件,这样handleSave将成为实例方法。答案 1 :(得分:0)
哇,这很有趣!钩子与类非常不同。我可以通过更改您的Item组件来使它正常工作。
const Item = React.memo(
function Item({ id, value, onChange, onSave }) {
console.log("render", id);
return (
<li>
<input
value={value}
onChange={event => onChange(id, event.target.value)}
/>
<button onClick={() => onSave(id)}>Save</button>
</li>
);
},
(prevProps, nextProps) => {
// console.log("PrevProps", prevProps);
// console.log("NextProps", nextProps);
return prevProps.value === nextProps.value;
}
);
通过将第二个参数添加到React.memo,它仅在prop值更改时更新。文档here解释说,这等效于类中的shouldComponentUpdate。
我不是Hooks的专家,所以任何可以确认或否认我的逻辑的人,请鸣叫并让我知道,但是我认为需要这样做的原因是因为在ItemList主体中声明了两个函数组件(handleChange和handleSave)实际上在每个渲染上都在更改。因此,发生地图时,每次为handleChange和handleSave传递新实例。 Item组件将它们检测为更改并引起渲染。通过传递第二个参数,您可以控制正在测试的Item组件,并且仅检查值prop是否不同,而忽略onChange和onSave。
可能会有更好的Hooks方法来执行此操作,但我不确定如何执行。我更新了代码示例,以便您可以看到它的工作状态。
https://codesandbox.io/s/keen-roentgen-5f25f?file=/src/App.js
答案 2 :(得分:0)
我获得了一些新见解(感谢Dan),我想我喜欢下面的内容。当然,对于这样一个简单的hello world示例来说,它看起来可能有点复杂,但是对于现实世界中的示例,它可能是一个很好的选择。
主要更改:
使用reducer + dispatch来保持状态。不需要,但要使其完整。然后,我们不需要onChange
处理程序的useCallback。
通过上下文传递调度。不需要,但要使其完整。否则,只需传递调度即可。
使用ItemWrapper
(或容器)组件。向树中添加其他组件,但随着结构的增长提供价值。这也反映了我们的情况:每个项目都有一个保存功能,需要整个项目。但是Item
组件本身没有。在这种情况ItemWrapper
中,ItemWithSave
可能被视为类似于save()提供程序。
为了反映一个更现实的情况,现在还存在一个“正在保存项目”状态,而另一个id仅在save()
函数中使用。
最终代码(另请参见:https://codesandbox.io/s/autumn-shape-k66wy?file=/src/App.js)。
const itemsFromServer = {
"1": {
id: "1",
otherIdForSavingOnly: "1-1",
value: "test",
isSaving: false
},
"2": {
id: "2",
otherIdForSavingOnly: "2-2",
value: "another row",
isSaving: false
}
};
function reducer(currentItems, action) {
switch (action.type) {
case "SET_VALUE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
value: action.value
}
};
case "START_SAVE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
isSaving: true
}
};
case "STOP_SAVE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
isSaving: false
}
};
default:
throw new Error();
}
}
ItemList
从服务器呈现所有项目export default function ItemList() {
const [items, dispatch] = useReducer(reducer, itemsFromServer);
return (
<ItemListDispatch.Provider value={dispatch}>
<ul>
{Object.values(items).map(item => (
<ItemWrapper key={item.id} item={item} />
))}
</ul>
</ItemListDispatch.Provider>
);
}
ItemWrapper
或ItemWithSave
function ItemWrapper({ item }) {
const dispatch = useContext(ItemListDispatch);
const handleSave = useCallback(
// Could be extracted entirely
async function save() {
if (item.value.length < 5) {
alert("Incorrect length.");
return;
}
dispatch({ type: "START_SAVE", id: item.id });
// Save to API
// eg. this will use otherId that's not necessary for the Item component
await new Promise(resolve => setTimeout(resolve, 1000));
dispatch({ type: "STOP_SAVE", id: item.id });
},
[item, dispatch]
);
return (
<Item
id={item.id}
value={item.value}
isSaving={item.isSaving}
onSave={handleSave}
/>
);
}
Item
const Item = React.memo(function Item({ id, value, isSaving, onSave }) {
const dispatch = useContext(ItemListDispatch);
console.log("render", id);
if (isSaving) {
return <li>Saving...</li>;
}
function onChange(event) {
dispatch({ type: "SET_VALUE", id, value: event.target.value });
}
return (
<li>
<input value={value} onChange={onChange} />
<button onClick={onSave}>Save</button>
</li>
);
});