关于reactjs:useLoopCallback —在循环内创建的组件的useCallback挂钩

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处理函数返回一个新函数,即使列表没有更改,也会导致按钮重新呈现。因此,它违反了useCallback的目的。

我想出了自己的钩子,称为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} />
      )}
   
  );
}


性能优化总是要付出代价的。有时,此成本低于要优化的操作,有时则更高。 useCallback是一个与useMemo非常相似的钩子,实际上您可以将其视为useMemo的一种特殊化,只能在函数中使用。例如,波纹管语句是等效的

1
2
3
4
const callback = value => value * 2

const memoizedCb = useCallback(callback, [])
const memoizedWithUseMemo = useMemo(() => callback, [])

因此,到目前为止,关于useCallback的每个断言都可以应用于useMemo

memoization的要点是保留旧值的副本,以便在我们获得相同的依赖项时返回,如果您要计算的是expensive的内容,这会很棒。看看下面的代码

1
2
3
const Component = ({ items }) =>{
    const array = items.map(x => x*2)
}

render个常量array都会创建一个常量,作为mapitems中执行的结果。因此,您会很想执行以下操作

1
2
3
const Component = ({ items }) =>{
    const array = useMemo(() => items.map(x => x*2), [items])
}

现在items.map(x => x*2)仅在items更改时才会执行,但这值得吗?最简洁的答案是不。通过这样做获得的性能是微不足道的,并且有时使用memoization会比仅执行每个渲染功能花费更多。这两个钩子(useCallbackuseMemo)在两个不同的用例中很有用:

  • 指称平等

当您需要确保引用类型不会仅仅因为shallow comparison

失败而不会触发重新渲染

  • 计算上昂贵的操作(仅useMemo)

类似这样的东西

1
const serializedValue = {item: props.item.map(x => ({...x, override: x ? y : z}))}

现在,您有理由进行memoized操作,并且每次props.item更改时都懒惰地检索serializedValue

1
const serializedValue = useMemo(() => ({item: props.item.map(x => ({...x, override: x ? y : z}))}), [props.item])

任何其他用例几乎总是值得再次重新计算所有值,React它非常高效,附加渲染几乎绝不会引起性能问题。请记住,有时您在优化代码上所做的努力可能会反过来,并生成大量多余/不必要的代码,这些代码不会产生太多好处(有时只会引起更多问题)。


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}
        />
      ))}
   
  );
};