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!
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.