47

I have read A Complete Guide to useEffect - Swimming Against the Tide at Overreacted.

The example shows that if we want to get the latest count, we can use useRef to save the mutable variable, and get it in async function laster:

function Example() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  useEffect(() => {
    // Set the mutable latest value
    latestCount.current = count;
    setTimeout(() => {
      // Read the mutable latest value
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });
  // ...
}

However, I can do the same thing by creating a variable outside the component function, such as:

import React, { useState, useEffect, useRef } from 'react';

// defined a variable outside function component
let countCache = 0;

function Counter() {
  const [count, setCount] = useState(0);
  countCache = count;       // set default value

  useEffect(() => {
    setTimeout(() => {
      // We can get the latest count here
      console.log(`You clicked ${countCache} times (countCache)`);
    }, 3000);
  });
  // ...
}

export default Counter;

Are both ways practical, or is there anything bad if I define the variable outside function component?

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
PJCHENder
  • 5,570
  • 3
  • 18
  • 35

2 Answers2

49

useRef will assign a reference for each component, while a variable defined outside a function component scope will only assigned once.

useRef reference life span is component's life span (it "dies" when the component unmounts, while JS variables are scope-blocked).

Hence, define constant purpose variables outside of the component scope:

// This statement will only called once
const DEFAULT_VALUE = 5;

function Component() {
  // use DEFAULT_VALUE.
}

Defining the same statement inside component's scope, will redefine it on every render:

// We can do better
function Component() {
  // Redefined on every render
  const DEFAULT_VALUE = 5;
}

Now for the question:

First, we can't actually reflect UI changed with outer scoped variables since changing them does not trigger render (only React API does).

Therefore the reflected value is its closure value.

let countCache = 0;

function Counter() {
  ...
  countCache = 0;

  useEffect(() => {
    countCache = count;
  });
  ...

  // closure value of countCache
  return <div>{countCache}</div>
}

Now, whats special with outer scope variables that they are global to the module itself, so using its value is global to all components referencing it (in the module).

For example if you want to count how many times the component mounted in your whole application life span, increase the variable inside useEffect on mount (couldn't find any other possible use-case).

let howMuchMounted = 0;

function Component() {
  useEffect(() => { howMuchMounted += 1, [] };
}

To reflect the differences of outer variable and useRef reference, in the next example, on button click, you may notice that the variable is global for both of the components, while the reference is always updated to current state value.

import React, { useEffect, useRef, useReducer } from "react";
import ReactDOM from "react-dom";

// defined a variable outside function component
let countCache = 0;

function Counter() {
  const [num, count] = useReducer((num) => num + 1, 0);

  const countRef = useRef(count);

  useEffect(() => {
    // set count value on count change
    countCache = num;
    countRef.current = num;
  }, [num]);

  return (
    <>
      <button onClick={count}>Count</button>
      <h3>state {num}</h3>
      <h3>variable {countCache}</h3>
      <h3>reference {countRef.current}</h3>
    </>
  );
}

export default function App() {
  return (
    <>
      <Counter />
      <hr />
      See what happens when you click on the other counter
      <hr />
      <Counter />
    </>
  );
}

Please see a follow up question on useEffect use cases, there are many common mistakes when working with useRef references inside useEffect.

Edit useRef vs Variable

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
  • I gave the wrong code. Now I replace `countCache = 0;` with `countCache = count;`. The results show as same as `useRef`, and can use `
    {countCache}
    ` in render as well.
    – PJCHENder Aug 11 '19 at 15:33
  • Still the answer the same, try making more than one counter see what happens. – Dennis Vash Aug 11 '19 at 15:34
  • I have add another counter, i.e., `counterOne`, `counterTwo`, in the component, and create two variables outside the function, i.e., `cacheCounterOne`, cacheCounterTwo`. However, I did not find anything weird. It looks fine... Could you give me more hints? – PJCHENder Aug 11 '19 at 15:46
  • I added an example check it out – Dennis Vash Aug 11 '19 at 15:48
  • Thanks! This sandbox is really helpful. I think I just figure it out. Many thanks. – PJCHENder Aug 11 '19 at 15:56
  • 2
    I found this answer just explain the basis difference of global versus local variables. Whenever the component is dead, its local scope variables are dead as well. The global variable is useful when you have a one file hosts all tiny related components which reference to some shared variables such as button theme, translation, color based devices, etc since the global variable will not die or re-init when any of these tiny components re-render. – James H. Oct 08 '21 at 08:56
  • @DennisVash think the answer has to be done more laconic, as the question is answered only in last abstract: `useRef will assign a reference for each component, while a variable defined outside a function component scope will only (be ?? ) assigned once.` -> hm, I can assign the variable multiple times from other places? `First, we can't actually reflect UI changed with outer scoped variables since changing them does not trigger render (only React API does).` - well, useRef.current assignment also does not trigger the render :) – Ross Nov 30 '21 at 17:51
  • The answer basically only explains that if we have 2+ components accessing the variable, yes `useRef` would be unique per component, but it makes no difference if you are sure that only one component is accessing the variable, for example counter and button issue. If it's the only difference, we may say these approaches are more equal than different – Ross Nov 30 '21 at 17:56
  • @RossIvantsiv So just to be sure, the variable kept outside the component will get garbage collected/destroyed just like any other variable kept inside component when component unmounts? I want to keep a class instance variable which I need to initialize only once. Would keeping it outside be a good idea? Or should I use useRef? – coder Apr 16 '22 at 14:37
18

There are differences between the following four possible variations:

  1. A variable inside the function component
  2. A variable outside the function component
  3. A state variable returned by useState()
  4. A variable ( an object with property 'current' ) returned by useRef()

The last 2 can only be used within the component.


Let's take each case:

1. A variable inside the function component

Initialization: Variables are always re-initialized both on every render and consequently in multiple component instances

Variable Updates: Can be updated except const

Render: No render is triggered by React, and hence updated values are not reflected


2. A variable outside the function component

Initialization: Variables are initialized only when the file is loaded. And the file is loaded only once, irrespective of how many components consume the exports from the file. This is the main difference compared to using useRef(). If there is only one component instantiation, it would be same as useRef().

Variable updates: Can be updated except const

Render: No render is triggered by React, and hence updated values are not reflected


3. A state variable returned by useState()

Initialization: Variables are initialized only when the component is mounted, irrespective of the number of renders. However, each component instance gets it's own copy of state variables.

Variable updates: Can be updated using the state updater function.

Render: Render is triggered by React after the state variable is updated ( multiple updates may be batched into a single render )


4. A variable ( an object with property 'current' ) returned by useRef()

Initialization: Variables are initialized only when the component is mounted, irrespective of the number of renders. However, this will independently initialize the variable for each component instance in contrast to when it occurs only once when using a variable outside the component

Variable updates: Can be updated using the 'current' property

Render: No render is triggered by React, and hence updated values are not reflected


Choosing any of the above will largely depend on the requirement, but we have the necessary tools in our toolbox to get almost all the use cases covered.

KJ Sudarshan
  • 2,694
  • 1
  • 29
  • 22