React组件包装在useCallback中

huangapple go评论57阅读模式
英文:

React component wrapped in useCallback

问题

根据官方的React文档,当尝试阻止某个父组件的子组件重新渲染时,需要将子组件包装在useMemo中,然后将在父组件中传递给子组件的函数包装在useCallback中。那么,这个实现的目的是什么呢?

const ParentComponent = () => {
  const [isFiltersOpen, setIsFiltersOpen ] = useState(false);

  const hasNoDevices = someHookReturnFromApi();
  const hasDevices = someHookReturnFromApi();
  const areAllDevicesLoaded = anotherHookButCallsApiForData();
  const missingOtherStuff = thisHookChecksIfSomeApisAreDoneLoading();
  const onSomething = useCallback(() => console.log(hasNoDevices), [hasNoDevices]);
  const sortedData = useMemo(() => hasDevices.sort(/* 进行一些排序... */));
  const filters = filtersFromApi();
  
  const onClosePanel = () =>  setIsFiltersOpen(false);

  const renderSection = useCallback(() => {
    if (hasNoDevices) {
      return (
        <EmptyState
          areAllDevicesLoaded={areAllDevicesLoaded}
          subtitle={'just.a.translation'}
        />
      );
    }

    if (missingOtherStuff) {
      return (
        <SectionEmptyState onSomething={onSomething} />
      );
    }

    return (
      <>
        <SectionData
          sortedData={sortedData}
          hasDevices={hasDevices}
        />

        <div>
          {isFiltersOpen && <Panel isOpen={isOpen} filters={filters} onClose={onClosePanel} />}
        </div>
      </>
    );
  }, [hasDevices, hasNoDevices, sortedData, areAllDevicesLoaded, onSomething, isFiltersOpen, isOpen, filters, onClosePanel]);

  return (
    <>
      <h1>Parent component</h1>
      {renderSection()}
    </>
  );
}

我的理解是,useCallback返回的是相同的函数引用,所以它有助于什么呢?它是否与内存分配有关?这样,当父组件重新渲染时,不会占用另一个内存空间来创建对所有内容的另一个引用?

编辑

评论中提供了很多有用的信息。唯一的问题是,我认为我的问题不够清晰,所以我将尝试缩短它!

我的问题归结为,在不将父组件包装在memo中的情况下,是否有意义在子组件中使用useCallback,例如 const componentVariable = useCallback(() => <SomeComponentMaybeWithProps />, []),它将位于其父组件的渲染中?

英文:

So, from what I understand from the official React docs, when trying to prevent a Child component of a certain Parent to re-render, you need to wrap the child in a useMemo, then wrap the functions inside the Parent, that are passed down to the Child in a useCallback, that being said, what is the point of this implementation:

const ParentComponent = () =&gt; {
const [isFiltersOpen, setIsFiltersOpen ] = useState(false);
const hasNoDevices = someHookReturnFromApi();
const hasDevices = someHookReturnFromApi();
const areAllDevicesLoaded = anotherHookButCallsApiForData();
const missingOtherStuff = thisHookChecksIfSomeApisAreDoneLoading();
const onSomething = useCallback(() =&gt; console.log(hasNoDevices), [hasNoDevices]);
const sortedData = useMemo(() =&gt; hasDevices.sort(// do some sorts...));
const filters = filtersFromApi();
const onClosePanel = () =&gt;  setIsFiltersOpen(false);
const renderSection = useCallback(() =&gt; {
if (hasNoDevices) {
return (
&lt;EmptyState
areAllDevicesLoaded={areAllDevicesLoaded}
subtitle={t(&#39;just.a.translation&#39;)}
/&gt;
);
}
if (missingOtherStuff) {
return (
&lt;SectionEmptyState onSomething={onSomething} /&gt;
);
}
return (
&lt;&gt;
&lt;SectionData
sortedData={sortedData}
hasDevices={hasDevices}
/&gt;
&lt;div&gt;
{isFiltersOpen &amp;&amp; &lt;Panel isOpen={isOpen} filters={filters} onClose={onClosePanel} /&gt;}
&lt;/div&gt;
&lt;/&gt;
);
}, [hasDevices, hasNoDevices, sortedData, areAllDevicesLoaded, onSomething, isFiltersOpen, isOpen, filters, onClosePanel]);
return (
&lt;&gt;
&lt;h1&gt;Parent component&lt;/h1&gt;
{renderSection()}
&lt;/&gt;
);

I mean useCallback, returns the same function, same reference, so how does this help? Does it have something to do with memory allocation? So that when the parent re-renders, another space in the memory won't be taken up to create another reference to all of that?

Edit

Lots of good information in the comments. The only thing is, I don't think my question is clear enough, so I'll try to shorten it!

My question boils down to, is there any meaning in using useCallback, on a child component, const componentVariable = useCallback(() =&gt; &lt;SomeComponentMaybeWithProps /&gt;, []), that will sit inside of the render of its parent component, as long as that parent component is not wrapped in a memo?

答案1

得分: 3

useCallback 缓存了您的函数及其依赖项,因此当这些依赖项未更改时,它不会重新渲染(后续渲染会将当前依赖项与先前依赖项进行比较)。所以是的,这是一种简单的性能优化。

这也正是在您的示例中发生的事情:renderSection 将保持不变,直到 [hasDevices, hasNoDevices, sortedData, areAllDevicesLoaded, onSomething, isFiltersOpen, isOpen, filters, onClosePanel] 中的一个或多个发生了更改。

您可以考虑实施 memo(https://react.dev/reference/react/memo)来不仅缓存函数,而且在函数未更改时完全跳过对 {renderSection()} 的重新渲染。

英文:

useCallback caches your function and its dependencies so that it won't re-render when said dependencies haven't changed (following renders compare current to previous dependencies). So yes, this is a simple performance optimization.

This also exactly what is happening in your example: renderSection will stay the same until one or more of [hasDevices, hasNoDevices, sortedData, areAllDevicesLoaded, onSomething, isFiltersOpen, isOpen, filters, onClosePanel] have changed.

You could think about implementing memo https://react.dev/reference/react/memo to not only cache the function but also skip re-rendering of {renderSection()} entirely when the function hasn't changed.

答案2

得分: 1

这是何时使用它们的区别。

useMemo

应该在需要进行昂贵计算时使用它。 (例如:假设您有一长串需要计算的操作,如果组件多次重新渲染,会减慢您的 JavaScript 线程)。

您所指的组件包装应该是 memo,而不是 useMemo,当没有任何相等性参数时,memo 会浅层比较其包装的组件的属性,并基于比较结果,如果两者相同,则返回先前的结果(无需重新渲染),如果有更改,则重新渲染所述比较。

useCallback

在 React 中,每次重新渲染都会重新计算其中的所有变量和函数,因此函数引用每次都会更改。如果将其传递给 memo 包装的组件,它可能会重新渲染,因为函数引用发生了变化。为了避免这种情况,您可以使用 useCallback,它只会在传递给它的依赖项更改时更改其引用。还请注意,如果在其中使用了任何值,例如未添加到依赖链的任何状态,这将导致问题,因为值不会更新,并且在调用函数时会有旧的值。

英文:

Here is the difference on when to use them.

useMemo

> It should be used when there are expensive calculations that needs to be done. (eg: assume you have a long list of calculations to be done which slows your js thread if there are multiple re-renders to the component).

What you are referring to as wrapping of the component should be memo and not useMemo which when used without any equality parameter shallow compares the props of the component wrapped with it and based on the comparison if both are same it returns the previous result (no-rerender), if there is a change it re-renders the said comparison.

useCallback

> In react on every re-render all the variable and function inside it is re-computed hence the function reference changes everytime. If you pass it to a memo-ed component it will possibly re-render since the function reference changes. To avoid that you can use useCallback which will only change its reference on dependency change provided to it. Also note that if any value like any state is being used inside it which is not added to the dependency chain it will cause issue since the value won't be updated and have stale values when the function gets called.

答案3

得分: 1

以下是翻译好的部分:

这是我将如何重构您的代码片段:

const ParentComponent = () => {
  const [isFiltersOpen, setIsFiltersOpen ] = useState(false);

  const hasNoDevices = someHookReturnFromApi();
  const hasDevices = someHookReturnFromApi();
  const areAllDevicesLoaded = anotherHookButCallsApiForData();
  const missingOtherStuff = thisHookChecksIfSomeApisAreDoneLoading();
  const onSomething = useCallback(() => console.log(hasNoDevices), [hasNoDevices]);
  const sortedData = useMemo(() => hasDevices.sort(// 进行一些排序...));
  const filters = filtersFromApi();
  
  const onClosePanel = useCallback(() => setIsFiltersOpen(false), []);

  return (
    <>
      <h1>父组件</h1>
      {hasNoDevices && !missingOtherStuff && (
        <EmptyState
          areAllDevicesLoaded={areAllDevicesLoaded}
          subtitle={t('just.a.translation')}
        />
      )}
      {!hasNoDevices && missingOtherStuff && (
        <SectionEmptyState onSomething={onSomething} />
      )}
      {!hasNoDevices && !missingOtherStuff && (
        <>
          <SectionData
            sortedData={sortedData}
            hasDevices={hasDevices}
          />
          <div>
            {isFiltersOpen && <Panel isOpen={isOpen} filters={filters} onClose={onClosePanel} />}
          </div>
        </>
      )}
    </>
  );
};

传递给子组件(EmptyStateSectionEmptyStateSectionDataPanel)的所有内容都已经被记忆化,除了onClosePanel(一个没有依赖项的回调)。看起来您似乎正在尝试做React已经自动处理的事情。您只需要关心这些子组件接收的props。确保这些props被记忆化,不会在ParentComponent重新渲染时创建新的引用就足够了。

这可能是样式偏好,但在组件中使用多个返回会有点难以阅读。在JSX中使用条件语句可以保持标记在一起。

英文:

Here's how I'd refactor your snippet:

const ParentComponent = () =&gt; {
const [isFiltersOpen, setIsFiltersOpen ] = useState(false);
const hasNoDevices = someHookReturnFromApi();
const hasDevices = someHookReturnFromApi();
const areAllDevicesLoaded = anotherHookButCallsApiForData();
const missingOtherStuff = thisHookChecksIfSomeApisAreDoneLoading();
const onSomething = useCallback(() =&gt; console.log(hasNoDevices), [hasNoDevices]);
const sortedData = useMemo(() =&gt; hasDevices.sort(// do some sorts...));
const filters = filtersFromApi();
const onClosePanel = useCallback(() =&gt; setIsFiltersOpen(false), []);
return (
&lt;&gt;
&lt;h1&gt;Parent component&lt;/h1&gt;
{hasNoDevices &amp;&amp; !missingOtherStuff &amp;&amp; (
&lt;EmptyState
areAllDevicesLoaded={areAllDevicesLoaded}
subtitle={t(&#39;just.a.translation&#39;)}
/&gt;
)}
{!hasNoDevices &amp;&amp; missingOtherStuff &amp;&amp; (
&lt;SectionEmptyState onSomething={onSomething} /&gt;
)}
{!hasNoDevices &amp;&amp; !missingOtherStuff &amp;&amp; (
&lt;&gt;
&lt;SectionData
sortedData={sortedData}
hasDevices={hasDevices}
/&gt;
&lt;div&gt;
{isFiltersOpen &amp;&amp; &lt;Panel isOpen={isOpen} filters={filters} onClose={onClosePanel} /&gt;}
&lt;/div&gt;
&lt;/&gt;
)}
&lt;/&gt;
);
};

Everything that is passed to the child components (EmptyState, SectionEmptyState, SectionData, Panel) is already memoized, except onClosePanel (a callback without dependencies). It kind of looked like you were trying to do what React does on its own already. You only need to worry about the props these child components receive. Making sure these props are memoized and not re-created with new references whenever ParentComponent renders is enough afaik.

It might be style preference, but several returns in a component are a bit hard to read. Conditionals in JSX instead keep the markup grouped together.

huangapple
  • 本文由 发表于 2023年7月3日 19:07:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/76604167.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定