Let's say I have a class designed to take care of complex operations called UtilityClass
and is set up as:
export default class UtilityClass {
constructor(name, setName) {
this.name = name
this.setName = setName
}
changeName(newName) {
this.setName(newName)
}
printName() {
console.log(this.name)
}
}
I want to initialise this class from my functional component and call its functions on button clicks:
import UtilityClass from './UtilityClass'
export function App(props) {
const utilityClass = useRef()
const [name, setName] = useState()
useEffect(() => {
utilityClass.current = new UtilityClass(name, setName)
}, [])
function setViaClass(newName) {
utilityClass.current.changeName(newName)
}
function printViaClass() {
utilityClass.current.printName()
}
return (
<>
<button onClick={() => setViaClass('Class')}>Set via class</button>
<button onClick={printViaClass}>Print via class</button>
</>
);
}
If I call UtilityClass.setName()
, the value of the state variable will change, but printing this.name
from within UtilityClass
will display undefined
.
Why is this.name
not synced with what the state value really is?
The reason I have UtilityClass
instead of having the logic directly in my functional component is that the logic should be called across multiple functional components, and I want to split my code into smaller readable bits.
CodePudding user response:
Your UtilityClass
has exactly one line where you assign to this.name
- in the constructor. As a result
printName() {
console.log(this.name)
}
will always print the initial name - the one passed when the class was constructed. Calling a state setter doesn't change the UtilityClass
because you only create it once, in the effect hook here.
useEffect(() => {
utilityClass.current = new UtilityClass(name, setName)
}, [])
You only create the class once, and you only pass in the name once, and you only set the property in the class once - so it never changes.
While it would be possible to refactor your class to account for this, to a limited extent - the whole concept doesn't really work well with React's functional paradigm. A much better approach that allows for the modularization of logic would be a custom hook that you call in each component that needs it. Here's an example of a hook that holds 2 state variables.
const usePersonInfo = () => {
const [name, setName] = React.useState('defaultname');
const [age, setAge] = React.useState(0);
return {
name,
setName,
age,
setAge,
// Example method implementing special logic
setAgeTo10: () => setAge(10)
};
};
function App(props) {
const { name, setName, age, setAge, setAgeTo10 } = usePersonInfo();
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={age} onChange={(e) => setAge(e.target.valueAsNumber)} type="number" />
<button onClick={setAgeTo10}>set age to 10</button>
</div>
);
}
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>
The above is the approach I like to use to separate out rendering logic from state manipulation logic, when a component starts to feel too large to fit both.
CodePudding user response:
Instead of using the default useState()
hook to manage your state, create a custom hook called useStateWithCallback()
like this
import { useState, useRef, useEffect, useCallback } from "react";
export const useStateWithCallback = (initState) => {
const [state, setState] = useState(initState);
const callbackRef = useRef(null);
const setStateCallback = useCallback((state, callback) => {
callbackRef.current = callback;
setState(state);
}, []);
useEffect(() => {
if (callbackRef.current) {
callbackRef.current(state);
callbackRef.current = null;
}
}, [state]);
return [state, setStateCallback];
}
In the App
component, use this new hook to create the state that has to be managed by that utility class
const [name, setName] = useStateWithCallback();
Now in the UtilityClass whenever you try to update the state using this.setName
provide a callback function as the second argument
export default class UtilityClass {
constructor(name, setName) {
this.name = name;
this.setName = setName;
}
changeName(newName) {
this.setName(newName, (newState) => {
this.name = newState;
});
}
printName() {
console.log(this.name);
}
}