你能控制基于状态变化的功能性反应组件的重新渲染吗?

Posted

技术标签:

【中文标题】你能控制基于状态变化的功能性反应组件的重新渲染吗?【英文标题】:Can you control the re-renders of a functional react component based on state change? 【发布时间】:2021-08-25 04:29:20 【问题描述】:

我有一个用于练习的基本电子商务应用程序。有一个名为“Shop”的功能组件有两种状态:

[products, setProducts] = useState([10ProductObjects])
and [cart, setCart] = useState([])

在第一个渲染周期中,加载了 10 个产品,每个 Product 组件都有一个“添加到购物车”按钮,用于将产品添加到购物车数组。 当我单击按钮时,会填充购物车数组并显示其长度。但是,这样做会再次重新渲染这 10 个产品,即使它们没有任何更改。

现在我看到了,因为其中一个状态发生了变化,即购物车数组,整个 Shop 组件再次呈现。依次渲染其子组件,包括未更改的子组件。

我尝试使用React.memo,但它没有帮助,因为“商店”上没有更改任何道具,而是状态正在改变。我还使用了useMemo 钩子,它有一些有趣的结果。

使用 products 数组作为依赖解决了额外的重新渲染问题,但新产品不再添加到购物车中。使用 [products, cart] 作为依赖项可以工作,但会带来原来的问题。

我知道可以使用 shouldComponentUpdate 来完成,但我需要功能组件中的那种灵活性。

注意:这是我的第一个问题,我很乐意听取任何反馈。

import React,  useState, useMemo  from 'react';
import fakeData from '../../fakeData';
import Product from '../Product/Product';

const Shop = () => 
console.log('[Shop.js] rendered')
const first10 = fakeData.slice(0, 10);
const [products, setProducts] = useState(first10);
const [cart, setCart] = useState([]);


const addProductHandler = (product) => 
    console.log(cart, product);
    const newCart = [...cart, product];
    console.log(newCart); 
    setCart(newCart);
    

let productsOnScreen = useMemo(() => 
    return products.map( prod => 
        return  <Product product=prod addProductHandler=addProductHandler />
    );
, [products])

 
return (
    <div className="shop-container">
        <div className="product-container">
            productsOnScreen
        </div>
        <div className="cart-container">
            <h3>this is cart</h3>
            <h5>Order Summary: cart.length</h5>
    </div>
    </div>
);
;

export default Shop;

【问题讨论】:

不要过早优化。您是否有特定的原因要避免重新渲染?如果您正确管理状态和效果,“额外”渲染在 99% 的情况下都不会成为问题。 React 进行了很好的优化,除非在极少数情况下(这不是其中之一),否则您极不可能遇到性能问题。 在这种特殊情况下,既不会更新产品列表,也不会以任何方式更改单个产品。那么为什么要重新渲染它们呢?是的,我有点过早地优化,或者更确切地说是学习优化,因为我更容易在像这样的简单应用程序中掌握理论。感谢您的评论。 即使他们的状态没有改变,他们是否重新渲染也没关系。这是完全正常和正常的,并且一直在发生。只要 render 函数正确地将 state 和 props 转换为正确的渲染输出,一点都没有关系。更担心你的状态、道具和效果,然后是“重新渲染”的次数,这在 99% 的情况下都无关紧要。如果您有任何依赖于已发生的重新渲染次数的逻辑,那么您做错了。 完全没问题,并且期望子组件在其包含的父组件重新渲染时会重新渲染。这就是 React 的工作原理。 我理解您要表达的意思。但我会等着看我是否对这个问题有一些不同的看法,然后再编辑帖子。 【参考方案1】:

Memoize addProductHandlerReact.useCallback 以便对它的引用在渲染之间不会改变:

const addProductHandler = React.useCallback((product) => 
    setCart(oldCart => 
        return [...oldCart, product];
    );
, [setCart]);

然后,用React.memo 记住&lt;Product&gt;。您没有发布该组件的代码,但它看起来像这样:

export const Product = React.memo((props) => 
  // normal functional component stuff

  return <>product component or whatever</>;
);

这些都是组件避免不必要的重新渲染所必需的。为什么?

    React.useCallback 允许通过引用进行比较以用于渲染之间的回调函数。如果你声明回调函数 every 渲染,这不起作用,就像你目前所做的那样。 React.memo 包装一个组件,使其能够根据其道具的浅显比较来渲染或渲染。默认情况下,React 组件不这样做,您必须使用 React.memo 显式启用它。

正如文档所述:

此方法仅作为性能优化存在。不要依赖它来“阻止”渲染,因为这可能会导致错误。

换句话说,您应该只在遇到速度变慢的情况下将其用作性能优化,而不是出于任何其他原因阻止渲染,并且仅当您实际上遇到性能问题时。 p>

如果这不起作用,则很可能您的 &lt;Product&gt; 道具之一在渲染之间仍然在变化(在浅层通过引用比较的情况下)。继续玩和记忆道具,直到你弄清楚哪些道具在渲染之间发生了变化。一种测试方法是在&lt;Product&gt; 中添加一个简单的React.useEffect(仅用于调试目的),它会在道具发生变化时提醒您:

React.useEffect(() => 
    console.log('product prop changed');
, [product]);

React.useEffect(() => 
    console.log('addProductHandler prop changed');
, [addProductHandler]);

// etc...

【讨论】:

非常感谢,这就像一个魅力。我将深入研究 React.useCallback 以更好地理解它。但我会牢记文档中引用的部分。

以上是关于你能控制基于状态变化的功能性反应组件的重新渲染吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何避免在反应功能组件中对“静态组件”进行不必要的重新渲染?

反应我必须重新渲染父母才能重新渲染孩子吗?

仅在重新渲染组件时才反应状态更新

在反应中更改状态时组件不会重新渲染

对道具更改做出反应重新渲染

在不改变状态、道具或父级的情况下反应子级渲染