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

Alv*_*dez 5 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>
  ); 
Run Code Online (Sandbox Code Playgroud)

但问题在于 onDeleteItem 将始终向 onClick 处理程序返回一个新函数,导致按钮重新呈现,即使列表没有更改。所以它违背了useCallback.

我想出了我自己的钩子,我称之为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]);
}
Run Code Online (Sandbox Code Playgroud)

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

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

这工作正常,但现在我想知道这个额外的逻辑是否真的让事情变得更快,或者只是增加了不必要的开销。任何人都可以提供一些见解吗?

编辑: 上述的替代方法是将列表项包装在它们自己的组件中。所以像这样:

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>
  ); 
}
Run Code Online (Sandbox Code Playgroud)

HMR*_*HMR 3

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

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

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>
  );
};
Run Code Online (Sandbox Code Playgroud)