DEV Community

Aleksei Berezkin
Aleksei Berezkin

Posted on • Edited on

2 JavaScript memory concerns for React developers

React provides a superb developer experience: you define states and props, combine components in a way you want — and everything magically updates yet stays consistent. But... What memory effects hide beneath this nice-looking code? Let's see!

1. Class members: functions vs arrow functions

Here are two very similar classes. What's the difference?

class A {
    x() {
        console.log('Hi!')
    }
}

class B {
    y = () => console.log('Hi!')
}
Enter fullscreen mode Exit fullscreen mode

Okay, okay you are right, y is this-bound 😉 But I wouldn't disturb you with such a trivial thing. There's an interesting memory implication I suggest you to spot.

⌛️

A.x resides on A prototype, and B.y copy resides on each B instance, meaning B instances consume more memory.

Writing the same using only functions makes this more prominent:

function A() {
}

A.prototype.x = function() {
    console.log('Hi!')
}

function B() {
    this.y = () => console.log('Hi!')
}
Enter fullscreen mode Exit fullscreen mode

A instances are completely empty!

Why is it important?

When implementing React class components we often need this-bound functions, and one possible option is an arrow function. In the following example each Button instance has its own handleClick member:

class Button {
    constructor(props) {
        this.props = props
    }
    render() {
        return <button onClick={this.handleClick} />
    }
    handleClick = () => console.log(this.props.message)
}
Enter fullscreen mode Exit fullscreen mode

Is it a problem?

In 99% of cases it's not — an arrow function instance is not that big. Just make sure you don't use it unless you need it. For example, if handleClick calls some other class method, it's better be defined as a simple function:

class Button {
    // ...
    handleClick = () => this.logMessage()
    logMessage() {
        console.log(this.props.message)
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Inner functions

What will the following code print? Or, in other words, is inner function referentially the same at each run?

function outer() {
    function inner() {
        console.log('Hi!')
    }
    return inner
}

console.log(outer() === outer())
Enter fullscreen mode Exit fullscreen mode

⌛️

The inner function is referentially different at each run, and the code outputs false.

Why is it important?

Inner functions are the common way to define handlers in React functional components:

function Button({message}) {
    function handleClick() {
        console.log(message)
    }
    return <button onClick={handleClick} />
}
Enter fullscreen mode Exit fullscreen mode

In this example a new handleClick is created on each function run, i.e. on each component render.

Someone told me useCallback can fix this

function Button({message}) {
    const handleClick = useCallback(function(m) {
        console.log(m)
    }, [message])
    return <button onClick={handleClick} />
}
Enter fullscreen mode Exit fullscreen mode

Now inner function(m) is created only when message changes, isn't it?

⌛️

No, useCallback can't override how JavaScript works, and function(m) is created at each component render.

Is it a problem?

Just like in previous example, it's fine in 99% of cases. However, if your handler doesn't need a closure over locals, you may define it outside the component:

function Button() {
    return <button onClick={handleClick} />
}

function handleClick() {
    console.log('Hi!')
}
Enter fullscreen mode Exit fullscreen mode

Further reading

Official explanation on hooks performance


Thanks for reading this. Do you know other JavaScript memory concerns useful to keep in mind?

Top comments (0)