useLoopCallback — useCallback hook for components created inside a loop
我想开始讨论建议的方法,该方法用于创建从循环内创建的组件接收参数的回调。
例如,如果我要填充具有"删除"按钮的项目列表,则我希望" onDeleteItem "回调知道要删除的项目的索引。像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | const onDeleteItem = useCallback(index => () => { setList(list.slice(0, index).concat(list.slice(index + 1))); }, [list]); return ( {list.map((item, index) => <span>{item}</span> <button type="button" onClick={onDeleteItem(index)}>Delete</button> )} ); |
但是问题是onDeleteItem总是会向onClick处理函数返回一个新函数,即使列表没有更改,也会导致按钮重新呈现。因此,它违反了
我想出了自己的钩子,称为useLoopCallback,它解决了这个问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import React, {useCallback, useRef} from"react"; export function useLoopCallback(code, dependencies) { const callback = useCallback(code, dependencies); const ref = useRef({map: new Map(), callback}); const store = ref.current; if (callback !== store.callback) { store.map.clear(); store.callback = callback; } return loopParam => { let loopCallback = store.map.get(loopParam); if (!loopCallback) { loopCallback = (...otherParams) => callback(loopParam, ...otherParams); store.map.set(loopParam, loopCallback); } return loopCallback; } } |
所以现在上面的处理程序看起来像这样:
1 2 3 | const onDeleteItem = useLoopCallback(index => { setList(list.slice(0, index).concat(list.slice(index + 1))); }, [list]); |
这很好用,但是现在我想知道这种额外的逻辑是否真的使事情变得更快或者仅仅是增加了不必要的开销。任何人都可以提供一些见识吗?
编辑:
替代方法是将列表项包装在其自己的组件中。像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | function ListItem({key, item, onDeleteItem}) { const onDelete = useCallback(() => { onDeleteItem(key); }, [onDeleteItem, key]); return ( <span>{item}</span> <button type="button" onClick={onDelete}>Delete</button> ); } export default function List(...) { ... const onDeleteItem = useCallback(index => { setList(list.slice(0, index).concat(list.slice(index + 1))); }, [list]); return ( {list.map((item, index) => <ListItem key={index} item={item} onDeleteItem={onDeleteItem} /> )} ); } |
性能优化总是要付出代价的。有时,此成本低于要优化的操作,有时则更高。
1 2 3 4 | const callback = value => value * 2 const memoizedCb = useCallback(callback, []) const memoizedWithUseMemo = useMemo(() => callback, []) |
因此,到目前为止,关于
1 2 3 | const Component = ({ items }) =>{ const array = items.map(x => x*2) } |
每
1 2 3 | const Component = ({ items }) =>{ const array = useMemo(() => items.map(x => x*2), [items]) } |
现在
- 指称平等
当您需要确保引用类型不会仅仅因为
失败而不会触发重新渲染
-
计算上昂贵的操作(仅
useMemo )
类似这样的东西
1 | const serializedValue = {item: props.item.map(x => ({...x, override: x ? y : z}))} |
现在,您有理由进行
1 | const serializedValue = useMemo(() => ({item: props.item.map(x => ({...x, override: x ? y : z}))}), [props.item]) |
任何其他用例几乎总是值得再次重新计算所有值,
List组件管理其自身的状态(列表),删除功能取决于该列表在其闭包中是否可用。因此,当列表更改时,删除功能也必须更改。
使用redux不会有问题,因为删除项目将通过分派操作来完成,并且将由始终具有相同功能的reducer进行更改。
React碰巧有一个useReducer挂钩,您可以使用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | import React, { useMemo, useReducer, memo } from 'react'; const Item = props => { //calling remove will dispatch {type:'REMOVE', payload:{id}} //no arguments are needed const { remove } = props; console.log('component render', props); return ( {JSON.stringify(props)} <button onClick={remove}>REMOVE</button> ); }; //wrap in React.memo so when props don't change // the ItemContainer will not re render (pure component) const ItemContainer = memo(props => { console.log('in the item container'); //dispatch passed by parent use it to dispatch an action const { dispatch, id } = props; const remove = () => dispatch({ type: 'REMOVE', payload: { id }, }); return <Item {...props} remove={remove} />; }); const initialState = [{ id: 1 }, { id: 2 }, { id: 3 }]; //Reducer is static it doesn't need list to be in it's // scope through closure const reducer = (state, action) => { if (action.type === 'REMOVE') { //remove the id from the list return state.filter( item => item.id !== action.payload.id ); } return state; }; export default () => { //initialize state and reducer const [list, dispatch] = useReducer( reducer, initialState ); console.log('parent render', list); return ( {list.map(({ id }) => ( <ItemContainer key={id} id={id} dispatch={dispatch} /> ))} ); }; |