Same frustrations with React Hooks

The first impression of React Hooks: Cool. The second impression is much more complicated. All the tutorials are using the same basic primitive type for the dependencies array. But as Paul Cowan wrote, dependencies array goes all the way to hell and back.

The comparison works great for primitive JavaScript types, but the problems can arise if one of the elements is an object or an array. Object.is will compare objects and arrays by reference, and there is no way to override this functionality and supply a custom comparator.

While refactoring some React Hooks #NotFancyAppChallenge, I moved from primitive props, into using a Hangman Javascript class. The React useState Hook returns a second argument that used to update the state. But you can not update a part of a class, only replace the entire thing.

Let’s say you are using a class:

class Letter{
  ch = "";
}

And a simple React Hooks app:

function App() {
   const [letter,setVal] = useState(new Letter());  
   const keydown = (event) => {
      // OOPS
   }
   useEffect(() => {
     window.addEventListener('keydown',keydown);    
     return (()=> {
       window.removeEventListener('keydown',keydown)
     })
   })
   return (
     last letter : {letter.ch}   
   );
 }

Now we have an OOPS since the function can only replace the whole object. We can not replace just the property “ch”. You might claim the React solved it with the “prevState”. Something like…

  const keydown = (event) => {
      setVal(prevState => {
        return { ...prevState, ch: event.key }
      });
  }

But this solution will work only while all the original object is exposed. If you have only computed values or some hidden values, you have a problem:

class Letter{
   ch = "";
   id =  Math.random();
   #pid = Math.random();
   get pid(){
      return this.#pid;
   }
 }

Running the Hook with the new letter will change the “ch”, and replace the old “id” with the same value. But the private properties, like “pid” will vanish.

when we update a state variable, we replace its value

Hooks FAQ

To handle changes in an object, React Hooks introduced the Reducer hook that instead of replacing the object, calls a function:

function reducer(state, key){
  state.ck = key;
  return state;
}  
function App() {
  const keydown = (event) => {
    setVal(event.key);
  }

  const [letter,setVal] = useReducer(reducer, new Letter());  
  ...
  ...

The reducer returns the same object, and the value is updated. But since we return the same object, React doesn’t know that the value had changed, and will not re-render the component. To update, you need to return a new object, and this brings us back to the same problem as in the useState.

In the end, (and hopefully meanwhile), the solution I found was to wrap the object I use, and return it nested:

function reducer(state, key){
  state.letter.ch = key;
  return {letter:state.letter};
}

function App() {
  const keydown = (event) => {
    setVal(event.key);
 }
   const [state,setVal] = useReducer(reducer, {letter: new Letter()});  

  useEffect(() => {
    window.addEventListener('keydown',keydown);    
    return (()=> {
      window.removeEventListener('keydown',keydown)
    })
  })

  return (
    <div>    
      <p>private id: {state.letter.pid}</p>
      <p>public id : {state.letter.id}</p>
      <p>last char : {state.letter.ch}</p>
    </div>
  );
}

Maybe the bad taste will pass once I’ll review Redax or another tool to separate the data.