From Headscratchers to Heroes - Demystifying Closures in React

Ah, closures. The infamous interview subject that induces perspiration even in experienced developers.

But guess what? Closures aren’t mythical monsters just there to ruin our day. They’re actually our undercover allies, ready to supercharge our code!

While most grasp the “what” of closures, the “why” and “how” often get lost in the theoretical fog. That’s where we come in!

Let’s ditch the dry textbook language and unveil the practical superpowers of closures.

Analogy for Closure

Bard provided a great analogy of closures.

Imagine a function, like a tiny robot, living inside another function, its bigger robot pal. The little robot gets a special bag from its parent, filled with all sorts of secrets (variables). Even when the big robot leaves, the little one still has access to everything in that bag - it remembers!

That’s a closure.

Pretty simple, right?

Now, let’s check out the code.

What is Closure?

Let’s start with functions and variables. What happens when we declare a function in JavaScript?

const greet = () => {
  const message = 'Hello World!';
};

console.log(message); // Uncaught ReferenceError: message is not defined

We create a local scope inside greet function which is invisible outside the function.

Similarly, a function created inside another function ( printMessage ) will have its own local scope, invisible to the function outside ( greet ). On the other hand the inner-most function ( printMessage ) will have access to all the variables declared outside.

const greet = () => {
  const message = 'Hello World!';

  const printMessage = () => {
    console.log(message); // Hello World!
  };
};

Let’s modify the code a bit to accept message as parameter.

const greet = (message) => {
  const printMessage = () => {
    console.log(message);
  };

  return printMessage;
};

const message1 = greet('Hello World!');
const message2 = greet('Hello Closure!');

message1(); // logs "Hello World!"
message2(); // logs "Hello Closure!"

Here,

When greet is called, the message argument is assigned a value. This value is captured by the closure, meaning the inner printMessage function can access it even after the greet function finishes execution. The greet function returns the printMessage function as its result. This means that even though the greet function itself no longer exists, the printMessage function still holds a reference to the captured message value. The message1 and message2 variables store the returned functions from two separate calls to greet with different messages.

In essence, closures allow functions to remember and access their enclosing scope even after the function itself has been returned or the outer function has finished executing.

This is all about the “what” of closures.

Now, let’s see how these undercover agents benefit our React code.

Closures in React

Closures in React come into play when dealing with event handlers and maintaining state across re-renders.

1. Event Handlers:

In React, event handlers are often defined as anonymous functions within JSX expressions. These anonymous functions can capture references to variables from the surrounding scope, creating closures.

const Counter = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  return (<button onClick={handleClick}>Click me: {count}</button>);
};

In this example, the handleClick function captures a reference to the count variable from its surrounding scope. This means that even if the count variable changes after the handleClick function is defined, the function will still have access to the latest value when it is called.

2. Hooks

useEffect, useCallback, or custom hooks leverage closures to encapsulate and manage state.

2.1. Custom hooks

Custom hooks are functions that use other hooks. They help in creating private data that is not accessible from the outside.

    const useCounter = (initialValue) => {
      const [count, setCount] = useState(initialValue);

      const increment = () => {
        setCount(count + 1);
      };

      return { count, increment };
    }

    const Counter = () => {
      const { count, increment } = useCounter(0);

      return (
        <div>
          <p>Count: {count}</p>
          <button onClick={increment}>Increment</button>
        </div>
      );
    }

In this example, the useCounter hook encapsulates the state (count) and the function to update it (increment). The closure formed by the inner function (increment) ensures that it always has access to the correct state.

2.2. useEffect hook

const Counter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      console.log(`Count: ${count}`);
    }, 1000);

    return () => {
      clearInterval(intervalId);
    };
  }, [count]);

  const handleClick = () => {
    setCount(count + 1)
  }

  return (<div>
    <p>Count: {count}</p>
    <button onClick={handleClick}>Increment</button>
    </div>);

}

The count variable is declared in the Counter component’s scope but is used within the useEffect callback function. This function captures the count variable due to closure, allowing it to access the current value of count even after the useEffect has been declared.

Stale Closure

If count dependency is not passed to useEffect, it provides stale data and the closure is termed as a stale closure.

 useEffect(() => {
    const intervalId = setInterval(() => {
      console.log(`Count: ${count}`);
    }, 1000);

    return () => {
      clearInterval(intervalId);
    };
  }, []);

Final Thoughts:

In React, JavaScript closures act as a secret weapon, empowering developers to write cleaner, more efficient, and maintainable code. They’re particularly crucial for managing state and handling events effectively.

Here’s how closures enhance React:

State Management Mastery:

Closures enable components and hooks to maintain access to their surrounding variables even after execution. This allows for seamless state updates within components and the creation of reusable custom hooks that encapsulate stateful logic.

Event Handling Excellence:

Closures ensure that event handlers can access and update component state correctly, even when those handlers are defined outside of the render function. This simplifies event handling and promotes code clarity.

Remember, closures aren’t interview monsters, they’re practical tools waiting to be mastered. So, embrace them and use them wisely!

Need help on your Ruby on Rails or React project?

Join Our Newsletter