DEV Community

Christopher Borchert
Christopher Borchert

Posted on

Using block-scoped variable at the module level

I was doing some code review today, and I found myself correcting a relatively junior dev's code, leaving the message, "Using let outside the component scope tends to be an antipattern. Use a useRef or a useState inside the component scope instead."

I think that this is generally true, and it was definitely true in the case of the other dev (they should have been using useState in this case). But I got to asking myself whether it is globally true. Is using block-scoped variables at the module level of React components, generally, an anti-pattern?

An example

For the purposes of demonstrating what I'm talking about, imagine the following code.

// MyComponent.js
import React, { useState } from "react";

let blockScopedVal = 0;

export default () => {
  const [myVal, setMyVal] = useState(0);

  return (
    <div>
      <button onClick={() => blockScopedVal ++)}>blockScopedVal: {blockScopedVal}</button>
      <button onClick={() => setMyVal((old) => old + 1)}>myVal: {myVal}</button>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

There are two basic implications of this code:

First, clicking on the first button will not cause a rerender of the component, similar to as if I had used a useRef.

For example, if I click the first button 10 times, I won't notice anything change in the UI, but when I click the second button, the first button's displayed value will jump to 10 and the second button's displayed value will increment correctly to 1.

Example 1

Second, if you have more than one <MyComponent />, they will share the variable blockScopedVal behind the scenes.

For example, imagine that I have two MyComponents. If I click the first button (the blockScopeVal incrementer) of the MyComponent 10 times, I won't notice anything change in the UI, but when I click its second button, the first button's displayed value will jump to 10 and the second button's displayed value will increment correctly to 1. The second MyComponent will still be at "blockScopedVal: 0" "myVal: 0". But, if I click the second MyComponent's second button, I'll have "blockScopedVal: 10" "myVal: 1". The blockScopedVal's value is shared between the two instances.

This is where using a block-scoped variable differs significantly from using a useRef.

Example 2

The question(s)

Is using block-scoped variables in React component modules an anti-pattern? If so, why? If not, what are some use cases?

My "don't do this" alarm went wild when I saw the let during code review, and in the case I was presented, my intuition was completely right, but I was unable to say why a priori this is definitively an antipattern. Imagine that you do want to share scope across all instances of the same component and only across the instances of that component 1 Is there any good reason not do to it this way, instead of reaching for some slick library?

I guess the difficulty is that I can't think of a great use case. Can any of you? Or, conversely, can you explain why we should never do this, ever?

I look forward to hearing from you all, and if you want to play around with the concept a bit, I made a code sandbox.


  1. ... and to do so in a way that doesn't trigger rerenders. Ok I might be reaching here since the number of use cases seems to be diminishing before my eyes. But still. Imagine.  

Top comments (0)