0

I think that's the right title. If it's misleading, I'll edit it. Also, I think I know the answer to this question already, but I'd like to check that against the community.

I was experimenting with React Hooks (which I like) and wrote up this little piece:

import React, { useState } from 'react';

import styles from './app.module.scss';

import Fretboard from './components/fretboard/fretboard';

export function App () {
const notes_sharps = [ 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E' ];
const notes_flats = [ 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B', 'C', 'Db', 'D', 'Eb', 'E' ];
  const [ current_notes, setCurrentNotes ] = useState ( notes_sharps );

  const onNotesToggle = () => {
    setCurrentNotes ( current_notes === notes_flats ? notes_sharps : notes_flats );
  };

  return (
    <div>
      <div className={ styles[ 'fretboard-holder' ] }>
        <Fretboard notes={ current_notes }/>
      </div>
      <div>
        <button onClick={ onNotesToggle}>Sharps/Flats</button>
      </div>
    </div>
  );
}

export default App;

The idea is pretty straightforward; the user clicks a button to toggle the display of notes, going back and forth between the sharps and flats array.

This code works with the first click. Not the second. After that, it stops working. You click the button, and nothing happens. No errors.

The logic is straightforward, so the reason should be:

  • First click, current_notes === notes_sharps, so we toggle to notes_flats.
  • Second click: current_notes === notes_flats so we toggle to notes_sharps...but we don't.

What's happening:

  • notes_sharps is preserved in the component via state.
  • First click: current_notes != to notes_flats. Set to notes_flats.
  • Second click: current_notes still !== notes_flats. So no change.

I wrote this code because I naively approached it as, "you can just substitute Hooks for Classes." But as I should have realized, you have to think differently. Even though when you trace out notes_flats and it looks identical, it's not the same array.

Changing the setCurrentNotes check to this:

setCurrentNotes ( current_notes.find(x => x === 'Gb') ? notes_sharps : notes_flats );

"Fixes" it because we're not comparing the Array anymore; we're comparing the content. I say "fixes" because I'm not sure this is what you'd actually want to do (why repeatedly reallocate constant Arrays).

Altering the code to set the notes_sharps and notes_flats outside of the App function allows the original toggle to run as expected (which makes sense. The variables are set outside of the function's scope, so will be the same arrays).

So one gotcha of hooks (this might be a good interview question) is that your values will be reallocated (maybe more precisely, will go out of scope and be recreated). Moving onNotesToggle outside the function won't work since you no longer have access to the state hook (current_notes, setCurrentNotes). You need to remember that the entire function will run when you change the state of the component it represents (that may be poorly worded).

const notes_sharps = [ 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E' ];
const notes_flats = [ 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B', 'C', 'Db', 'D', 'Eb', 'E' ];

export function App () {
  const [ current_notes, setCurrentNotes ] = useState ( notes_sharps );

  const onNotesToggle = () => {
    setCurrentNotes ( current_notes === notes_flats ? notes_sharps : notes_flats );
  };

...

A request for clarity in follow-up discussion:

  • If you say, "you shouldn't write React code that way," please explain exactly why.
  • Please follow up the above with exactly how you'd suggest.
  • Please don't add additional libs or context to the example. Keep it a simple toggle.

Although I've already gotten to the bottom of it, I figured I'd put this out there for discussion and learning, and who knows, maybe I'm still missing something. Thanks kindly.

Tim Consolazio
  • 4,802
  • 2
  • 19
  • 28

2 Answers2

0

This isn't exactly a problem of Hooks vs Classes. You can change most of your use cases from classes to hooks. The problem is the way you're comparing your current_notes. You're using the strict equality operator (===) to compare it to notes_flats. In this case, since they also reference different things, it should always return false.

In this case, if you wanted to make sure they are exactly equal, then you can follow How to check if two arrays are equal.

If you don't then want to check if they are both exactly equal, then you can maybe just make a state for const [isSharpNotes, setIsSharpNotes] = useState(true) and pass notes_sharp if true else notes_flats.

Leomar Amiel
  • 469
  • 3
  • 13
  • 1
    I get that this isn't a problem explicitly of hooks vs. classes (I say as much). The problem is really more about function scope. But the documentation for Hooks does imply the switch is very straightforward, I found that somewhat misleading; when I focused on it I saw the issue immediately, but somebody really rooted in OOP dev might find similar confusion. What's interesting is, even the React tutorial moves you from a function to a class when you add things like this. So the answer I'm looking for more is, "when thinking in React," because a generic usage wouldn't include state hooks. – Tim Consolazio Apr 08 '21 at 13:31
0

I'll post an answer to my own question and let the community sort it out.

With a bit more reading, I saw that (naturally) the problem I detail above was anticipated; you can use useRef to persist the value.

This refactored code works:

import React, { useState, useRef } from 'react';
import styles from './app.module.scss';
import Fretboard from './components/fretboard/fretboard';

export function App () {

  const notes_sharps = useRef([ 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E' ]);
  const notes_flats = useRef([ 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B', 'C', 'Db', 'D', 'Eb', 'E' ]);

  const [ current_notes, setCurrentNotes ] = useState ( notes_sharps );

  const onNotesToggle = () => {
    setCurrentNotes ( current_notes === notes_flats ? notes_sharps : notes_flats );
  };

  return (
    <div>
      <div className={ styles[ 'fretboard-holder' ] }>
        <Fretboard strings={ 6 } notes={ current_notes.current }/>
      </div>
      <div>
        <button onClick={ onNotesToggle}>Sharps/Flats</button>
      </div>
    </div>
  );
}

export default App;

Note that current_notes now uses the "current" property, which reflects the API usage of useRef.

Is there value to doing it this way rather than just using the external consts; practically, probably not. But since frameworks are intended to manage plumbing, this might be a more "React Hooks" approach to the issue.

Tim Consolazio
  • 4,802
  • 2
  • 19
  • 28