Performant React Cookbook:回调道具

In this article we will have a look at how to pass a callback function to React components across multiple examples, and how to optimize each case. This will prevent wasted re-renders, enable you to avoid performance bottlenecks from the beginning and make your app much faster for your users. Let’s get started! 🚀

在本文中,我们将研究如何在多个示例中将回调函数传递给React组件,以及如何优化每种情况。 这将防止浪费的重新渲染,使您从一开始就避免性能瓶颈,并使用户的应用程序更快。 让我们开始吧! 🚀

Simple callback function

简单的回调函数

The very first case we will talk about the most common pattern we do when we need to pass a callback function to a React component.

在第一种情况下,我们将讨论需要将回调函数传递给React组件时最常用的模式。

First let’s have a look in the context of class components:

首先,让我们看一下类组件的上下文:

class Form extends React.Component {
    ...
    render() {
        return <Button onClick={() => { this.setState({ isClicked: true }) }} />
    }
}

At first, everything seems to be fine here. However, this approach is going to create a new instance of the function passed inside of the onClick prop. Since this is passing a new prop to the Button component, it will re-render completely.

一开始,这里一切似乎都很好。 但是,此方法将创建在onClick道具内部传递的函数的新实例。 由于这会将新的道具传递给Button组件,因此它将完全重新渲染。

In many cases, this is not a really big issue unless your Button component is very heavy. But this approach can cause big problems when you need to render some heavier components. There are couple of way to prevent `Button` re-rendering in this case:

在许多情况下,除非您的Button组件非常沉重,否则这并不是一个大问题。 但是,当您需要渲染一些较重的组件时,此方法可能会导致大问题。 在这种情况下,有几种方法可以防止“按钮”重新呈现:

  1. Bad way — ignore updates to the onClick prop (using shouldComponentUpdate, for example) and call the value which was passed during the first render. This may cause some unexpected issues down the line, when you would expect that you’re using the last version of the callback while you won’t. It may take a while to inspect the root cause for such bugs. So better be aware of it.

    坏方法-忽略对onClick道具的更新(例如,使用shouldComponentUpdate ),并调用在第一次渲染期间传递的值。 当您期望使用的是最新版本的回调,而您却不会使用时,可能会导致一些意外的问题。 检查此类错误的根本原因可能需要一段时间。 因此最好注意这一点。

  2. Good way — pass the same reference to a function on each render. This can be easily achieved:

    好的方法-在每个渲染器上将相同的引用传递给函数。 这很容易实现:
class Form extends React.Component {
    ...
    onClick = () => {
        this.setState({ isClicked: true })
    }


    render() {
        return <Button onClick={this.onClick} />
    }
}

This way only one instance of onClick callback will be created and it will persist between re-renders. Button will never re-render because of a changed onClick property, as it always stays the same.

这样,将仅创建一个onClick回调实例,并且该实例将在重新渲染之间保持不变。 Button永远不会因为onClick属性的更改而重新呈现,因为它始终保持不变。

Let’s have a look at the same problem in the context of functional components as things behave a bit differently there.

让我们看一下功能组件上下文中的相同问题,因为那里的行为有些不同。

const Form = () => {
  const [isClicked, setClicked] = React.useState()


  return (
    <Button
      onClick={() => {
        setClicked(true)
      }}
    />
  );
};

You can observe the same pattern here: On each render, a new instance of the callback function is created, which is unnecessary.

您可以在此处观察到相同的模式:在每个渲染上,都将创建一个新的回调函数实例,这是不必要的。

The better way to avoid that inside a stateless functional component is to use the React.useCallback hook. It will persist the callback reference unless some of its dependencies changed (we will talk about dependencies later). Here is an example how to use it:

避免在无状态功能组件中出现的更好方法是使用React.useCallback挂钩。 除非某些依赖关系发生了变化,否则它将保留回调引用(稍后将讨论依赖关系)。 这是一个示例如何使用它:

const Form = () => {
  const [isClicked, setClicked] = React.useState()
  const onClick = React.useCallback(() => setClicked(true), [])


  return <Button onClick={onClick} />
};

This will reuse the onClick function between re-renders of the Form component.

这将在重新Form组件之间重用onClick函数。

We just went through very basic examples where you see that preventing re-rendering doesn’t take much of your time and it might save you tons of time when you need to optimize your application later.

我们只是看了一些非常基本的示例,在这些示例中,您可以看到阻止重新渲染并不会花费很多时间,并且在以后需要优化应用程序时可以节省大量时间。

具有依赖项的回调函数 (Callback function with dependencies)

In this section we are going to talk about a more complicated case when we have dependencies to props, state inside of our callback functions.

在本节中,我们将讨论一个更复杂的情况,当我们对props有依赖性时,在回调函数中声明状态。

Let’s start again by looking at the example using class components:

让我们从使用类组件的示例开始重新开始:

class Form extends React.Component {
    ...
    render() {
        const { email, password } = this.state


        return (
            ...
            <Button onClick={() => {
                this.props.onSubmit({ email, password })
            }} />
        )
    }
}

Here we face example of the same problem as in the previous section and we can apply the same pattern we did before by defining the callback as a static method on the component class:

在这里,我们面临与上一节相同的问题的示例,并且可以通过在组件类上将回调定义为静态方法来应用与之前相同的模式:

class Form extends React.Component {
    ...
    onSubmit = () => {
        const { email, password } = this.state
        this.props.onSubmit({ email, password })
    }


    render() {
        return (
            ...
            <Button onClick={this.onSubmit} />
        )
    }
}

Let’s move on the how to resolve this problem with stateless functional components. Let’s have a look at the solution from the previous section:

让我们继续介绍如何使用无状态功能组件解决此问题。 让我们看一下上一节中的解决方案:

const Form = ({ onSubmit }) => {
    const onFormSubmit = React.useCallback(() => onSubmit(), [onSubmit])


    return (
        ...
        <Button onClick={onFormSubmit} />
    )
}

As you see we have [onSubmit] as the second argument of React.useCallback hook. This will make sure that onFormSubmit is updated only when onSubmit property is changed.

如您所见,我们将[onSubmit]作为React.useCallback挂钩的第二个参数。 这将确保onSubmit属性更改时才更新onFormSubmit

具有依赖关系的回调函数,在每个渲染器上都会更新 (Callback function with dependencies which are updated on each render)

There is another case when dependencies of your callbacks are updated on each render so in order to keep your callbacks up-to-date, you need to re-create them on each render. Let’s see how we can fix that.

另一种情况是,在每个渲染上都会更新回调的依赖关系,因此为了使回调保持最新,您需要在每个渲染上重新创建它们。 让我们看看如何解决这个问题。

One possible way would be to have a pull mechanism to get the latest state of data when the callback is triggered. Here is an example for class based component:

一种可能的方式是具有一种拉动机制,以在触发回调时获取最新的数据状态。 这是基于类的组件的示例:

class Form extends React.Component {
    ...
    onSubmit = () => {
        // or simply get data from this.state or this.props
        const payload = this.getLatestData()
        this.props.onSubmit({ payload })
    }


    render() {
        return (
            ...
            <Button onClick={this.onSubmit} />
        )
    }
}

This approach is very similar to the case when we were taking data from this.state in one of our previous examples. So here we simply pull the data whenever we need it.

这种方法与我们在先前示例之一中从this.state获取数据的情况非常相似。 因此,在这里我们仅在需要时提取数据。

Let’s have a look at a functional component, where things will be a bit more complicated.

让我们看一个功能组件,那里的事情会更加复杂。

const Form = ({ onSubmit }) => {
    const [email, setEmail] = React.useState()
    const emailRef = React.useRef()
    emailRef.current = email


    const onFormSubmit = React.useCallback(() => {
        onSubmit(emailRef.current)
    }, [onSubmit, emailRef])


    return (
        ... code changing email
        <Button onClick={onFormSubmit} />
    )
}

As you see we had to use React.useRef in order to have a mutable source from where we can read data whenever we need it. Keep in mind that any reference to emailRef object is always the same, so we can theoretically skip it from the list of dependencies of React.useCallback, as it never changes.

如您所见,我们必须使用React.useRef来获得可变的源,以便在需要时可以从中读取数据。 请记住,对emailRef对象的任何引用始终是相同的,因此理论上我们可以从React.useCallback的依赖项列表中跳过它,因为它永远不会改变。

To make things look a bit better you can use useLatestValue hook which hides ref details under the hood and you receive a better looking API:

为了使事情看起来更好,您可以使用useLatestValue钩子,该钩子将ref详细信息隐藏在useLatestValue ,您会收到一个外观更好的API:

const useLatestValue = (value) => {
  const valueRef = React.useRef(value)
  valueRef.current = value
  return React.useCallback(() => valueRef.current, [])
};

Here is an example using the hook in a component:

这是在组件中使用钩子的示例:

const Form = ({ onSubmit }) => {
    const [email, setEmail] = React.useState()
    const getEmail = useLatestValue(email)


    const onFormSubmit = React.useCallback(() => {
        onSubmit(getEmail())
    }, [onSubmit, getEmail])


    return (
        ... code changing email
        <Button onClick={onFormSubmit} />
    )
}

You don’t need to create refs yourself and simply get a function that returns the latest value.

您无需自己创建引用,只需获取一个返回最新值的函数即可。

The reference to this function is also static, so we can skip it from dependencies list of React.useCallback.

对该函数的引用也是静态的,因此我们可以从React.useCallback依赖项列表中跳过它。

数组元素的回调函数 (Callback functions for array elements)

In this section we are going to solve another callback puzzle when you need to iterate over array elements and use the data from the array inside of you callback functions which are passed to rendered components. Here is an example of how this might look in practice:

在本节中,当您需要遍历数组元素并使用回调函数中传递给渲染组件的数组中的数据时,我们将解决另一个回调难题。 这是一个在实际中看起来如何的示例:

class List extends React.Component {
    render() {
        return (
            ...
            {
                this.props.elementsData.map((element) => (
                    <Element
                        onClick={() => this.props.onElementClicked(element)}
                        data={element}
                    />
                ))
            }
        )
    }
}

The obvious problem here is that on each re-render of the List component, you will have all Element components re-rendered due to the onClick property being updated. I assume this is usually not what we want. There are multiple ways of solving this problem, but we will have a look at the most optimized one:

这里的明显问题是,在每次重新渲染List组件时,由于onClick属性已更新,因此您将重新渲染所有Element组件。 我认为这通常不是我们想要的。 解决此问题的方法有多种,但我们将介绍最优化的一种:

The main idea is that you should adjust the logic of Element component a little bit, so that it passes the data property (or any other) to our callback property. This enables us to have a static onClick callback and the data will arrive there through its arguments. Here’s an example:

主要思想是您应该稍微调整Element组件的逻辑,以便它将data属性(或任何其他属性)传递给我们的回调属性。 这使我们可以有一个静态的onClick回调,数据将通过其参数到达那里。 这是一个例子:

class List extends React.Component {
    onElementClicked = (elementData) => {
        this.props.onElementClicked(elementData)
    }


    render() {
        return (
            {
                this.props.elementsData.map((element) => (
                    <Element
                        onClick={this.onElementClicked}
                        data={element}
                    />
                ))
            }
        )
    }
}


const Element = ({ data, onClick }) => {
    // also possible to create a memoized callback with the use of useCallback hook
    // and then pass it down to button element
    // const onClickCb = useCallback(() => onClick(data), [data])
    return (
        <div>
            <p>{data.title}</p>
            <button onClick={() => onClick(data)}>Open</button>
        </div>
    )
}

This way if we have any update prevention mechanism (e.g. React.memo or React.PureComponent), Element won’t be updated as on each re-render of List component we will receive the same properties (references to objects will be the same). It may save you quite a lot of time if you implement everything in an optimized way from the very beginning.

这样,如果我们有任何更新阻止机制(例如React.memo或React.PureComponent), Element将不会更新,因为每次重新呈现List组件时,我们都会收到相同的属性(对对象的引用将相同) 。 如果从一开始就以优化的方式实施所有操作,则可以节省大量时间。

Let’s have a look at another use case. So you have this.props.onElementClicked inside the List component and you need to pass there not just element data, but also the index of the item which was clicked. This may sound challenging, however we can still solve it with one single line of code. You may think for a little while and then compare your ideas with solution example:

让我们看一下另一个用例。 因此,您在List组件中具有this.props.onElementClicked ,不仅需要传递元素数据,还需要传递被单击项的索引。 这听起来可能具有挑战性,但是我们仍然可以用一行代码来解决它。 您可能会思考片刻,然后将您的想法与解决方案示例进行比较:

class List extends React.Component {
    onElementClicked = (elementData) => {
        this.props.onElementClicked({
            data: elementData,
            index: this.props.elementsData.indexOf(elementData),
        })
    }


    render() {
        return (
            {
                this.props.elementsData.map((element) => (
                    <Element
                        onClick={this.onElementClicked}
                        data={element}
                    />
                ))
            }
        )
    }
}

That would be one solution, however it’s not optimal for very big collections of data (5–10k+ elements). To improve the performance for big collections of data, and still be able to get an index, you may need to pass the index property to the `Element` when rendering and then pass it to onClick property. This can be done using an object or a separate argument.

那将是一种解决方案,但是对于大量数据(5-10k +个元素)而言,它并不是最佳选择。 为了提高大型数据收集的性能并仍然能够获得索引,您可能需要在呈现时将index属性传递给Element,然后将其传递给onClick属性。 可以使用一个对象或一个单独的参数来完成。

A sample implementation could be:

一个示例实现可以是:

const Element = ({ data, index, onClick }) => {
  return (
    <div>
      <p>{data.title}</p>
      <button onClick={() => onClick({ data, index })}>Open</button>
    </div>
  );
};

This way all 3 properties of the Element component are staying the same between re-renders and your onElementClicked callback is receiving the data based on which element was clicked.

这样,在重新渲染之间, Element组件的所有3个属性都保持不变,并且onElementClicked回调将根据单击的元素接收数据。

Keep in mind that stateless functional components don’t prevent updates in case parent re-renders. Even when the same properties are passed functional components will update, to prevent that you should wrap them with React.memo

请记住,在父级重新渲染的情况下,无状态功能组件不会阻止更新。 即使传递了相同的属性,功能组件也会更新,以防止您应该使用React.memo包装它们

Let’s apply all principles which we saw above to a stateless functional component so we know how to make it optimized as well as it might be not that obvious. Let’s start from this implementation of List component:

让我们将上面看到的所有原理应用于无状态功能组件,以便我们知道如何优化它,而且它可能并不那么明显。 让我们从List组件的此实现开始:

const List = ({ onElementClicked, elementsData }) => {
    return (
        {
            elementsData.map((element) => (
                <Element
                    onClick={() => onElementClicked(element)}
                    data={element}
                />
            ))
        }
    )
}

Let’s apply the knowledge we gained from previous examples to solve this case for stateless functional component rendering a list of elements. Solution will looks like that:

让我们应用从先前示例中获得的知识来解决这种无状态功能组件呈现元素列表的情况。 解决方案将如下所示:

const List = ({ onElementClicked, elementsData }) => {
    const onClickedCb = React.useCallback((elementsData) => {
        onElementClicked(elementsData)
    }, [onElementClicked])


    return (
        ...
        {
            elementsData.map((element) => (
                <Element
                    onClick={onClickedCb}
                    data={element}
                />
            ))
        }
    )
}


const Element = ({ data, onClick }) => {
    return (
        <div>
            <p>{data.title}</p>
            <button onClick={() => onClick(data)}>Open</button>
        </div>
    )
}

As you see it’s exactly the same approach we had before for class components, so we had to adjust a bit the behaviour onClick callback inside of Element component. If you need to extend the onElementClicked call with some more data like index, you can apply the same approach as we did in the previous example with class List component.

如您所见,这与我们以前对类组件使用的方法完全相同,因此我们不得不对Element组件内部的onClick回调行为进行一些调整。 如果您需要使用诸如index之类的更多数据来扩展onElementClicked调用,则可以使用与上一示例中的类List组件相同的方法。

如果无法更改元素组件怎么办? (What if you cannot change Element component?)

In this section we will solve the same problem as in the previous section, with the only difference that we are not allowed to modify the Element component (considering it could be node module). One possible way would be to memoize the callback functions you pass to Element components so that you always pass the same references between re-renders. This is how the solution would look like for class List component:

在本节中,我们将解决与上一节相同的问题,唯一的区别是我们不允许修改Element组件(考虑它可能是节点模块)。 一种可能的方法是记住传递给Element组件的回调函数,以便始终在重新渲染之间传递相同的引用。 这是类List组件的解决方案的样子:

class List extends React.Component {
    getElementOnClicked = memoize((index, elementData) => () => {
        this.props.onElementClicked(data: elementData)
    })


    render() {
        return (
            {
                this.props.elementsData.map((element, index) => (
                    <Element
                        onClick={this.getElementOnClicked(index, element)}
                        data={element}
                    />
                ))
            }
        )
    }
}

In the example above, the memoized function returns the same reference to a callback function (elementData) => … whenever you pass the same index again. On the first render of List component it will create callbacks for each index, but on the second one it will return you memoized callbacks created during the first render. More about the concept of memoization can be found in the lodash.memoize() docs or any other utility library.

在上面的示例中,每当您再次传递相同的index时, memoized函数都会对回调函数(elementData) => …返回相同的引用。 在List组件的第一个渲染中,它将为每个index创建回调,但是在第二个渲染中,它将返回您在第一个渲染期间创建的记忆回调。 可以在lodash.memoize()文档或任何其他实用程序库中找到有关lodash.memoize()概念的更多信息。

However, this approach becomes very memory consuming if you have a lot of elements in your list, so we will try and improve that:

但是,如果列表中有很多元素,则此方法会占用大量内存,因此我们将尝试改进此方法:

class List extends React.Component {
    onClickCallbacks = new WeakMap()


    getElementOnClicked = (index, elementData) => {
        if (this.onClickCallbacks.has(index))
            return this.onClickCallbacks.get(index)


        const callback = () => {
            this.props.onElementClicked(elementData)
        }


        this.onClickCallbacks.set(index, callback)
        return callback
    }




    render() {
        return (
            {
                this.props.elementsData.map((element, index) => (
                    <Element
                        onClick={this.getElementOnClicked(index)}
                        data={element}
                    />
                ))
            }
        )
    }
}

The main idea here is that we store callbacks for each index inside of the WeakMap object. The main difference to Map is that once nothing is referencing the callback anymore, it will be removed from memory. This allows to avoid memory leaks.

这里的主要思想是我们在WeakMap对象内部存储每个索引的回调。 与Map的主要区别在于,一旦不再引用回调,它将从内存中删除。 这样可以避免内存泄漏。

With the current implementation of the render function, all callbacks will be references all the time so it won’t give you a big benefit on a large collection of elements. But, if you apply a virtualized principle when rendering a list of elements, you’ll gain all benefits of callbacks being removed from memory for elements which are not rendered. Here are couple of resources about virtualized approach for rendering big lists of data:

使用render函数的当前实现,所有回调都将一直被引用,因此它不会为您带来大量元素的大量好处。 但是,如果在呈现元素列表时应用虚拟化原理,则将获得从内存中删除未呈现元素的回调的所有好处。 以下是有关呈现大数据列表的虚拟化方法的一些资源:

关于分析的一些话 (Some words about profiling)

Profiling is another very important topic which deserves a separate article. But the most basic thing you can do is to check if your app have problems with wasted renders using the great why-did-you-render library.

概要分析是另一个非常重要的主题,值得单独撰写。 但是,您可以做的最基本的事情是使用强大的“ 为什么要渲染”库来检查应用程序是否存在浪费的渲染问题。

If that’s not enough you can try out the timeline tool in Chrome devtools as shown in this article.

如果还不够,您可以尝试使用Chrome devtools中的时间轴工具,如本文所示。

If you want to see an impact of unoptimized rendering you can play around with this sample project from Kent C. Dodds.

如果要查看未优化渲染的影响,可以尝试使用Kent C. Dodds的此示例项目

外卖 (Takeaways)

I hope you enjoyed reading and learned some small tricks on how to overcome the problem when you need to pass dynamic callbacks into your components. Our experience showed it’s easier and faster to keep the app in the optimized state than refactoring everything when the app got too slow.

我希望您喜欢阅读并且学到了一些小技巧,以了解当需要将动态回调传递到组件中时如何解决该问题。 我们的经验表明,将应用程序保持在优化状态比在速度太慢时重构所有内容更容易,更快捷。

翻译自: https://medium.com/joyn-tech-blog/performant-react-cookbook-callback-props-99a7cfdaa25f

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Over 66 hands-on recipes that cover UI development, animations, component architecture, routing, databases, testing, and debugging with React Today’s web demands efficient real-time applications and scalability. If you want to learn to build fast, efficient, and high-performing applications using React 16, this is the book for you. We plunge directly into the heart of all the most important React concepts for you to conquer. Along the way, you’ll learn how to work with the latest ECMAScript features. You’ll see the fundamentals of Redux and find out how to implement animations. Then, you’ll learn how to create APIs with Node, Firebase, and GraphQL, and improve the performance of our application with Webpack 4.x. You’ll find recipes on implementing server-side rendering, adding unit tests, and debugging. We also cover best practices to deploy a React application to production. Finally, you’ll learn how to create native mobile applications for iOS and Android using React Native. By the end of the book, you’ll be saved from a lot of trial and error and developmental headaches, and you’ll be on the road to becoming a React expert. What You Will Learn Gain the ability to wield complex topics such as Webpack and server-side rendering Implement an API using Node.js, Firebase, and GraphQL Learn to maximize the performance of React applications Create a mobile application using React Native Deploy a React application on Digital Ocean Get to know the best practices when organizing and testing a large React application

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值