简体   繁体   English

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

[英]useLoopCallback -- useCallback hook for components created inside a loop

I'd like to start a discussion on the recommended approach for creating callbacks that take in a parameter from a component created inside a loop.我想开始讨论创建回调的推荐方法,这些回调接收来自循环内创建的组件的参数。

For example, if I'm populating a list of items that will have a "Delete" button, I want the "onDeleteItem" callback to know the index of the item to delete.例如,如果我要填充具有“删除”按钮的项目列表,我希望“onDeleteItem”回调知道要删除的项目的索引。 So something like this:所以像这样:

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

But the problem with this is that onDeleteItem will always return a new function to the onClick handler, causing the button to be re-rendered, even when the list hasn't changed.但问题在于 onDeleteItem 将始终向 onClick 处理程序返回一个新函数,导致按钮重新呈现,即使列表没有更改。 So it defeats the purpose of useCallback .所以它违背了useCallback的目的。

I came up with my own hook, which I called useLoopCallback , that solves the problem by memoizing the main callback along with a Map of loop params to their own callback:我想出了我自己的钩子,我称之为useLoopCallback ,它通过记住主回调以及循环参数映射到他们自己的回调来解决这个问题:

import React, {useCallback, useMemo} from "react";

export function useLoopCallback(code, dependencies) {
  const callback = useCallback(code, dependencies);
  const loopCallbacks = useMemo(() => ({map: new Map(), callback}), [callback]);

  return useCallback(loopParam => {
    let loopCallback = loopCallbacks.map.get(loopParam);
    if (!loopCallback) {
      loopCallback = (...otherParams) => loopCallbacks.callback(loopParam, ...otherParams);
      loopCallbacks.map.set(loopParam, loopCallback);
    }
    return loopCallback;
  }, [callback]);
}

So now the above handler looks like this:所以现在上面的处理程序看起来像这样:

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

This works fine but now I'm wondering if this extra logic is really making things faster or just adding unnecessary overhead.这工作正常,但现在我想知道这个额外的逻辑是否真的让事情变得更快,或者只是增加了不必要的开销。 Can anyone please provide some insight?任何人都可以提供一些见解吗?

EDIT: An alternative to the above is to wrap the list items inside their own component.编辑:上述的替代方法是将列表项包装在它们自己的组件中。 So something like this:所以像这样:

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

Performance optimizations always come with a cost.性能优化总是有代价的。 Sometimes this cost is lower than the operation to be optimized, sometimes is higher.有时这个成本低于要优化的操作,有时更高。 useCallback it's a hook very similar to useMemo , actually you can think of it as a specialization of useMemo that can only be used in functions. useCallback它是一个与useMemo非常相似的钩子,实际上您可以将其视为useMemo一种特殊化,只能在函数中使用。 For example, the bellow statements are equivalents例如,下面的语句是等价的

const callback = value => value * 2

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

So for now on every assertion about useCallback can be applied to useMemo .所以现在关于useCallback每个断言都可以应用于useMemo

The gist of memoization is to keep copies of old values to return in the event we get the same dependencies, this can be great when you have something that is expensive to compute. memoization的要点是保留旧值的副本,以便在我们获得相同的依赖项时返回,当您有一些计算expensive东西时,这可能很棒。 Take a look at the following code看看下面的代码

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

Uppon every render the const array will be created as a result of a map performed in items .在每次render , const array将作为在items执行的map的结果而创建。 So you can feel tempted to do the following因此,您可能会很想执行以下操作

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

Now items.map(x => x*2) will only be executed when items change, but is it worth?现在items.map(x => x*2)只会在items改变时执行,但这值得吗? The short answer is no.最简洁的答案是不。 The performance gained by doing this is trivial and sometimes will be more expensive to use memoization than just execute the function each render.通过这样做获得的性能是微不足道的,有时使用memoization比在每次渲染时执行函数更昂贵。 Both hooks( useCallback and useMemo ) are useful in two distinct use cases:两个钩子( useCallbackuseMemo )在两个不同的用例中都很有用:

  • Referencial equality参照平等

When you need to ensure that a reference type will not trigger a re render just for failing a shallow comparison当您需要确保引用类型不会因为shallow comparison失败而触发重新渲染时

  • Computationally expensive operations (only useMemo )计算成本高的操作(仅useMemo

Something like this像这样的东西

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

Now you have a reason to memoized the operation and lazily retrieve the serializedValue everytime props.item changes:现在您有理由memoized操作并在每次props.item更改时懒惰地检索serializedValue

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

Any other use case is almost always worth to just re compute all values again, React it's pretty efficient and aditional renders almost never cause performance issues.任何其他用例几乎总是值得再次重新计算所有值, React非常高效,附加渲染几乎不会导致性能问题。 Keep in mind that sometimes your efforts to optimize your code can go the other way and generate a lot of extra/unecessary code, that won't generate so much benefits (sometimes will only cause more problems).请记住,有时您优化代码的努力可能会反其道而行,并生成大量额外/不必要的代码,这不会产生太多好处(有时只会导致更多问题)。

The List component manages it's own state (list) the delete functions depends on this list being available in it's closure. List 组件管理它自己的状态(列表),删除功能取决于这个列表在它的闭包中是否可用。 So when the list changes the delete function must change.因此,当列表更改时,删除功能必须更改。

With redux this would not be a problem because deleting items would be accomplished by dispatching an action and will be changed by a reducer that is always the same function.使用 redux 这不会成为问题,因为删除项目将通过分派 action 来完成,并且将由始终具有相同功能的 reducer 更改。

React happens to have a useReducer hook that you can use: 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>
  );
};

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM