Home > OS >  Controlling State Variable from Class
Controlling State Variable from Class

Time:07-13

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);
  }
}
  • Related