如何根据项目列表防止使用React Hooks,功能组件和功能进行不必要的重新渲染

时间:2020-07-13 19:59:40

标签: reactjs

要呈现的项目列表

给出项目列表(来自服务器):

const itemsFromServer = {
  "1": {
    id: "1",
    value: "test"
  },
  "2": {
    id: "2",
    value: "another row"
  }
};

每个项目的功能组件

我们要渲染每个项目,但仅在必要时进行某些更改:

const Item = React.memo(function Item({ id, value, onChange, onSave }) {
  console.log("render", id);

  return (
    <li>
      <input
        value={value}
        onChange={event => onChange(id, event.target.value)}
      />
      <button onClick={() => onSave(id)}>Save</button>
    </li>
  );
});

带有handleSave函数的ItemList函数组件,需要对其进行记忆。

并且可以保存每个单独的项目:

function ItemList() {
  const [items, setItems] = useState(itemsFromServer);

  const handleChange = useCallback(
    function handleChange(id, value) {
      setItems(currentItems => {
        return {
          ...currentItems,
          [id]: {
            ...currentItems[id],
            value
          }
        };
      });
    },
    [setItems]
  );

  async function handleSave(id) {
    const item = items[id];

    if (item.value.length < 5) {
      alert("Incorrect length.");
      return;
    }

    await save(item);

    alert("Save done :)");
  }

  return (
    <ul>
      {Object.values(items).map(item => (
        <Item
          key={item.id}
          id={item.id}
          value={item.value}
          onChange={handleChange}
          onSave={handleSave}
        />
      ))}
    </ul>
  );
}

当只有一项更改时,如何防止每个Item不必要地重新渲染?

当前,在每个渲染上都会创建一个新的handleSave函数。使用useCallback时,items对象包含在依赖项列表中。

可能的解决方案

  1. value作为参数传递给handleSave,从而从items的依赖项列表中删除handleSave对象。在此示例中,这可能是一个不错的解决方案,但是由于多种原因,在现实生活中它并不是首选(例如,更多的参数等)。

  2. 使用单独的组件ItemWrapper,可以在其中记住handleSave函数。

function ItemWrapper({ item, onChange, onSave }) {
  const memoizedOnSave = useCallback(onSave, [item]);

  return (
    <Item
      id={item.id}
      value={item.value}
      onChange={onChange}
      onSave={memoizedOnSave}
    />
  );
}
  1. 使用useRef()钩子,在对items的每次更改中,将其写入ref,并在items函数内部从ref读取handleSave

  2. >
  3. 在该状态下保留变量idToSave。设置为保存。然后使用useEffect(() => { /* save */ }, [idToSave])触发保存功能。 “被动地”。

问题

以上所有解决方案对我而言似乎都不理想。 是否还有其他方法可以防止在每个渲染器上为每个handleSave创建一个新的Item函数,从而防止不必要的重新渲染?这样吗?

CodeSandbox:https://codesandbox.io/s/wonderful-tesla-9wcph?file=/src/App.js

3 个答案:

答案 0 :(得分:0)

我要问的第一个问题:重新渲染真的有问题吗?

您是对的,react将为您在此处具有的每个函数重新调用每个渲染,但是您的DOM不应有太大改变,这可能没什么大不了的。

如果在渲染Item时进行了大量计算,则可以记住繁琐的计算。

如果您真的想优化此代码,我会在这里看到不同的解决方案:

  1. 最简单的解决方案:将ItemList更改为类组件,这样handleSave将成为实例方法。
  2. 使用应该正常工作的外部表单库:您在final-formformikreact-hook-form中拥有强大的表单库
  3. 另一个外部库:您可以尝试为此特定用例构建的recoiljs

答案 1 :(得分:0)

哇,这很有趣!钩子与类非常不同。我可以通过更改您的Item组件来使它正常工作。

const Item = React.memo(
  function Item({ id, value, onChange, onSave }) {
    console.log("render", id);

    return (
      <li>
        <input
          value={value}
          onChange={event => onChange(id, event.target.value)}
        />
        <button onClick={() => onSave(id)}>Save</button>
      </li>
    );
  },
  (prevProps, nextProps) => {
    // console.log("PrevProps", prevProps);
    // console.log("NextProps", nextProps);
    return prevProps.value === nextProps.value;
  }
);  

通过将第二个参数添加到React.memo,它仅在prop值更改时更新。文档here解释说,这等效于类中的shouldComponentUpdate。

我不是Hooks的专家,所以任何可以确认或否认我的逻辑的人,请鸣叫并让我知道,但是我认为需要这样做的原因是因为在ItemList主体中声明了两个函数组件(handleChange和handleSave)实际上在每个渲染上都在更改。因此,发生地图时,每次为handleChange和handleSave传递新实例。 Item组件将它们检测为更改并引起渲染。通过传递第二个参数,您可以控制正在测试的Item组件,并且仅检查值prop是否不同,而忽略onChange和onSave。

可能会有更好的Hooks方法来执行此操作,但我不确定如何执行。我更新了代码示例,以便您可以看到它的工作状态。

https://codesandbox.io/s/keen-roentgen-5f25f?file=/src/App.js

答案 2 :(得分:0)

我获得了一些新见解(感谢Dan),我想我喜欢下面的内容。当然,对于这样一个简单的hello world示例来说,它看起来可能有点复杂,但是对于现实世界中的示例,它可能是一个很好的选择。

主要更改:

  • 使用reducer + dispatch来保持状态。不需要,但要使其完整。然后,我们不需要onChange处理程序的useCallback。

  • 通过上下文传递调度。不需要,但要使其完整。否则,只需传递调度即可。

  • 使用ItemWrapper(或容器)组件。向树中添加其他组件,但随着结构的增长提供价值。这也反映了我们的情况:每个项目都有一个保存功能,需要整个项目。但是Item组件本身没有。在这种情况ItemWrapper中,ItemWithSave可能被视为类似于save()提供程序。

  • 为了反映一个更现实的情况,现在还存在一个“正在保存项目”状态,而另一个id仅在save()函数中使用。

最终代码(另请参见:https://codesandbox.io/s/autumn-shape-k66wy?file=/src/App.js)。

初始状态,来自服务器的项目

const itemsFromServer = {
  "1": {
    id: "1",
    otherIdForSavingOnly: "1-1",
    value: "test",
    isSaving: false
  },
  "2": {
    id: "2",
    otherIdForSavingOnly: "2-2",
    value: "another row",
    isSaving: false
  }
};

管理状态的还原器

function reducer(currentItems, action) {
  switch (action.type) {
    case "SET_VALUE":
      return {
        ...currentItems,
        [action.id]: {
          ...currentItems[action.id],
          value: action.value
        }
      };

    case "START_SAVE":
      return {
        ...currentItems,
        [action.id]: {
          ...currentItems[action.id],
          isSaving: true
        }
      };

    case "STOP_SAVE":
      return {
        ...currentItems,
        [action.id]: {
          ...currentItems[action.id],
          isSaving: false
        }
      };

    default:
      throw new Error();
  }
}

我们的ItemList从服务器呈现所有项目

export default function ItemList() {
  const [items, dispatch] = useReducer(reducer, itemsFromServer);

  return (
    <ItemListDispatch.Provider value={dispatch}>
      <ul>
        {Object.values(items).map(item => (
          <ItemWrapper key={item.id} item={item} />
        ))}
      </ul>
    </ItemListDispatch.Provider>
  );
}

主要解决方案ItemWrapperItemWithSave

function ItemWrapper({ item }) {
  const dispatch = useContext(ItemListDispatch);

  const handleSave = useCallback(
    // Could be extracted entirely
    async function save() {
      if (item.value.length < 5) {
        alert("Incorrect length.");
        return;
      }

      dispatch({ type: "START_SAVE", id: item.id });

      // Save to API
      // eg. this will use otherId that's not necessary for the Item component
      await new Promise(resolve => setTimeout(resolve, 1000));

      dispatch({ type: "STOP_SAVE", id: item.id });
    },
    [item, dispatch]
  );

  return (
    <Item
      id={item.id}
      value={item.value}
      isSaving={item.isSaving}
      onSave={handleSave}
    />
  );
}

我们的Item

const Item = React.memo(function Item({ id, value, isSaving, onSave }) {
  const dispatch = useContext(ItemListDispatch);

  console.log("render", id);

  if (isSaving) {
    return <li>Saving...</li>;
  }

  function onChange(event) {
    dispatch({ type: "SET_VALUE", id, value: event.target.value });
  }

  return (
    <li>
      <input value={value} onChange={onChange} />
      <button onClick={onSave}>Save</button>
    </li>
  );
});