0

I've been reading on why useRef is useful (e.g. in this SO answer and in the articles it links to) and it does make sense to me. However I notice that in my code I've "simply" solved the issue of how to store state in a functional component in a way that does not trigger re-renders by keeping the state as a global-scoped variable declared in the same file as the functional component.

I realize this isn't appropriate if the same component is rendered at the same time in multiple places on the DOM, as useRef supplies different state to different simultaneously rendered components whereas a file-scoped variable would be shared.

Is my mental model and assumptions correct and are there any other use cases or distinct advantages of useRef versus a file-scoped variable?

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
Marcus Junius Brutus
  • 26,087
  • 41
  • 189
  • 331

1 Answers1

2

Is my mental model and assumptions correct ...

Not in the general case, even with your caveat. There are a couple of issues:

  1. As you say, components can be instantiated more than once at the same thing (think: items in a list), but with your file-scoped (I assume you mean module-scoped) variable, all instances would use the same variable, causing cross-talk between the instances. With useRef, they'll each have their own non-state instance data.

    Here's an example of the difference:

       
       const { useState, useRef } = React;
       
       let moduleScopedVariable = 0;
       
       const TheComponent = () => {
           const ref = useRef(0);
       
           // Synthetic use case: counting renders
           ++ref.current;
           ++moduleScopedVariable;
       
           return (
               <div className="the-component">
                   <div>Render count (ref): {ref.current}</div>
                   <div>Render count (var): {moduleScopedVariable}</div>
               </div>
           );
       };
       
       const ids = [0, 1, 2];
       
       const Example = () => {
           const [counter, setCounter] = useState(0);
       
           return (
               <div>
                   Counter: {counter}
                   <input type="button" value="Increment" onClick={() => setCounter(c => c + 1)} />
                   <div>{ids.map((id) => <TheComponent key={id} />)}</div>
               </div>
           )
       };
       
       const root = ReactDOM.createRoot(document.getElementById("root"));
       root.render(<Example />);
       
       
       
       .the-component {
           border: 1px solid black;
           margin: 4px;
       }
       
       
       
       <div id="root"></div>
       
       <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
       <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
       
       
  2. It's not just at the same time, there's crosstalk with a single instance that's mounted and unmounted over time:

    Here's an example:

       
       const { useState, useRef } = React;
       
       let moduleScopedVariable = 0;
       
       const TheComponent = () => {
           const ref = useRef(0);
       
           // Synthetic use case: counting renders
           ++ref.current;
           ++moduleScopedVariable;
       
           return (
               <div className="the-component">
                   <div>Render count (ref): {ref.current}</div>
                   <div>Render count (var): {moduleScopedVariable}</div>
               </div>
           );
       };
       
       const Example = () => {
           const [flag, setFlag] = useState(true);
       
           return (
               <div>
                   Flag: {String(flag)}
                   <input type="button" value="Toggle" onClick={() => setFlag(b => !b)} />
                   {flag && <TheComponent />}
               </div>
           )
       };
       
       const root = ReactDOM.createRoot(document.getElementById("root"));
       root.render(<Example />);
       
       
       
       .the-component {
           border: 1px solid black;
           margin: 4px;
       }
       
       
       
       <div id="root"></div>
       
       <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
       <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
       
       
  3. It also means the data remains lying around when the component is unmounted, which depending on what the data is could be an issue.

Fundamentally, it's great to reuse static data by closing over a module-scoped constant, but anything that changes within the component should be stored in state (in any of various guises) if it affects how the component renders, or a ref (usually) if not.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thanks, I do qualify my statement with this caveat. I've used it on sort of "big" components that I knew would be rendered only once. I understand from your answer that this is the only reason (plus, I guess, `useRef` is cleaner and more explicit). – Marcus Junius Brutus Oct 07 '22 at 10:43
  • 1
    @MarcusJuniusBrutus - Sorry, I've updated to address the bit in your question about "at the same time". :-) – T.J. Crowder Oct 07 '22 at 10:49
  • @TusharShahi - Thanks. I've updated to address that. – T.J. Crowder Oct 07 '22 at 10:49