Search code examples
javascriptreactjsreact-hooksreact-functional-component

Class components referencing and setting their own static variables - what is the functional component equivalent?


When using class components, I am able to set static variables that will persist on each further instance of the class. This allows me to set a kind of firstRun state on a component, even when calling the same component multiple times.

I have tried to create this behaviour in a functional component, but when the component is used multiple times, it seems to forget its states, ref, etc each time.

In the code below, I have two components, one functional and one class. Each one of these components is called three times in App. In the class component I am able to directly set ClassComponent.firstRun which I am then able to reference in further inclusions of the same component. In the functional component I have tried the same with useRef, but this only seems to work per instance of the component, and gets forgotten on each new component.

const { useEffect, useRef } = React;

const FunctionalComponent = (props) => {
  const { id } = props;
  const mounted = useRef(false);
  useEffect(() => {
    if (!mounted.current) {
      console.log('Initial functional component load...', id);
      mounted.current = true;
    } else {
      console.log('Functional component has already been initialised.', id)
    }
  }, []); 
  return (
    <div>Hello, functional component!</div>
  );
};

class ClassComponent extends React.Component {
  constructor(props) {
    super(props);
    this.createSomething();
  }
  
  static firstRun = true;
  
  createSomething() {
    if (ClassComponent.firstRun) {
      console.log('Initial class component load...', this.props.id);
      ClassComponent.firstRun = false;
    } else {
      console.log('Class component has already been initialised.', this.props.id);
    }
  }

  render() {
    return (
      <div>Hello, class component!</div>
    );
  }
}

function App() {
  return (
    <div>
      <ClassComponent id="class-component-1" />
      <ClassComponent id="class-component-2" />
      <ClassComponent id="class-component-3" />
      <FunctionalComponent id="functional-component-1" />
      <FunctionalComponent id="functional-component-2" />
      <FunctionalComponent id="functional-component-3" />
    </div>
  )
}

ReactDOM.render(<App />, document.querySelector("#app"));
<div id="app"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>

The class component works good - I get one log for the initial load, then two logs saying it has already intialised - great! The functional component however is logging three initial load messages.

I have tried using useState, but as I understand this only works for re-renders, and not separate occurences of the component. This also seems to be the same situation when using useRef.

I have read about function closures, and tried to crudely implement one with the code below, but this again is just giving me three Initialised... logs:

const checkInitial = () => {
  let initial = true; 
  return {
    get: function() { return initial },  
    set: function(state) { initial = state; }
  };
}
...
const FunctionalComponent = (props) => {
  const { id } = props;
  const mounted = useRef(false);
  useEffect(() => {
    const firstRun = checkInitial();
    if (firstRun.get()) {
      console.log('Initial...', id);
      firstRun.set(true);
    } else {
      console.log('Already run...', id);
    }
  }, []); 
  return (
    <div>Hello, functional component!</div>
  );
};

I believe that setting a context variable may be able to get around this, but I'd rather not use that right now. I'm also aware that I can lift the state up to the parent, but I want to avoid this as it will most likely cause re-renders.

This situation seems to easy to solve with class components, but these are now obsolete. Is there an easy way to do this purely using functional functions/components?

Cheers!


Solution

  • Any hook, whether useState or useRef, is per instance of the component.

    If you really want a static variable1, just do exactly the same as you did with the class component - store it on the function object itself:

    function FunctionComponent({ id }) {
      if (FunctionComponent.firstRun) {
        console.log('Initial function component render...', id);
        FunctionComponent.firstRun = false;
      } else {
        console.log('Function component has already been rendered before.', id);
      }
      return (
        <div>Hello, function component!</div>
      );
    }
    FunctionComponent.firstRun = true;
    

    It would be more customary though to just declare a variable in the module scope where the function component is defined:

    let firstRun = true;
    function FunctionComponent({ id }) {
      if (firstRun) {
        console.log('Initial function component render...', id);
        firstRun = false;
      } else {
        console.log('Function component has already been rendered before.', id);
      }
      return (
        <div>Hello, function component!</div>
      );
    }
    

    If you don't want the log to appear every time the component is rendered, but only once, when it is mounted, you can use an effect or the initialiser of a state:

    let firstRun = true;
    function FunctionComponent({ id }) {
      useEffect(() => {
        if (firstRun) {
          console.log('Initial function component mount...', id);
          firstRun = false;
        } else {
          console.log('Function component has already been mounted elsewhere.', id);
        }
      }, []);
      return (
        <div>Hello, function component!</div>
      );
    }
    
    let firstRun = true;
    function FunctionComponent({ id }) {
      const [isFirst] = useState(() => {
        if (firstRun) {
          firstRun = false;
          return true;
        } else {
          return false;
        }
      });
      return (
        <div>Hello, {isFirst && 'first'} function component!</div>
      );
    }
    

    1: You probably don't want a static variable. It's ok for constants, but as soon as you have stateful static variable, it's essentially global state. Avoid that.