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.