useLoopCallback-在循环内创建的组件的useCallback钩子

时间:2019-08-26 18:22:50

标签: reactjs react-hooks usecallback

我想开始讨论推荐的方法,该方法用于创建从循环内创建的组件接收参数的回调。

例如,如果我要填充具有“删除”按钮的项目列表,则希望“ onDeleteItem”回调知道要删除的项目的索引。像这样:

  const onDeleteItem = useCallback(index => () => {
    setList(list.slice(0, index).concat(list.slice(index + 1)));
  }, [list]);

  return (
    <div>
      {list.map((item, index) =>
        <div>
          <span>{item}</span>
          <button type="button" onClick={onDeleteItem(index)}>Delete</button>
        </div>
      )}
    </div>
  ); 

但是,这样做的问题是onDeleteItem总是会向onClick处理程序返回一个新函数,从而即使按钮列表未更改,按钮也将重新呈现。因此,它违反了useCallback的目的。

我想出了自己的钩子,称为 useLoopCallback ,它可以解决问题:

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

所以现在上面的处理程序看起来像这样:

  const onDeleteItem = useLoopCallback(index => {
    setList(list.slice(0, index).concat(list.slice(index + 1)));
  }, [list]);

这很好用,但是现在我想知道这种额外的逻辑是否真的使事情变得更快或者仅仅是增加了不必要的开销。谁能提供一些见识?

编辑: 替代方法是将列表项包装在其自己的组件中。像这样:

function ListItem({key, item, onDeleteItem}) {
  const onDelete = useCallback(() => {
    onDeleteItem(key);
  }, [onDeleteItem, key]);

  return (
    <div>
      <span>{item}</span>
      <button type="button" onClick={onDelete}>Delete</button>
    </div>
  );
}

export default function List(...) {
  ...

  const onDeleteItem = useCallback(index => {
    setList(list.slice(0, index).concat(list.slice(index + 1)));
  }, [list]);

  return (
    <div>
      {list.map((item, index) =>
        <ListItem key={index} item={item} onDeleteItem={onDeleteItem} />
      )}
    </div>
  ); 
}

2 个答案:

答案 0 :(得分:2)

List组件管理其自身的状态(列表),删除功能取决于该列表在其闭包中是否可用。因此,当列表更改时,删除功能也必须更改。

使用redux不会有问题,因为删除项目将通过分派操作来完成,并且将由始终具有相同功能的reducer进行更改。

React碰巧有一个useReducer钩子可供您使用:

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 (
    <div>
      <div>{JSON.stringify(props)}</div>
      <div>
        <button onClick={remove}>REMOVE</button>
      </div>
    </div>
  );
};
//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 (
    <div>
      {list.map(({ id }) => (
        <ItemContainer
          key={id}
          id={id}
          dispatch={dispatch}
        />
      ))}
    </div>
  );
};

答案 1 :(得分:1)

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

const callback = value => value * 2

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

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

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

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

由于render中执行array会创建一个map常量items。因此,您会很想做以下事情

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

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

  • 参考平等

当您需要确保引用类型不会仅仅因为shallow comparison失败而触发重新渲染时

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

类似这样的东西

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

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

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

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