Reputation: 25
I'm refactoring a previously made class-based React app to be functional with the use of Hooks. It's a simple drum machine app. I'm using .map to populate the page from an array of objects containing the data for reach drum pad and it's associated sound.
{soundBank.map((sound) => (
<DrumPad
key={sound.id}
id={sound.id}
letter={sound.letter}
src={sound.src}
handleDisplay={handleDisplay}
/>
))}
After refactoring, everything seems to work except for playing individual drum pads using onKeyPress. Here's what my code looked like as a class-based component:
handleKeyPress = event => {
if (event.keyCode === this.props.letter.charCodeAt()) {
this.audio.play();
this.audio.currentTime = 0;
this.props.handleDisplay(this.props.id);
}
render() {
return (
<div
className="drum-pad"
id={this.props.id}
onClick={this.handleClick.bind(this)}
onKeyPress={this.handleKeyPress.bind(this)}
>
<h1>{this.props.letter}</h1>
<h2>{this.props.id}</h2>
<audio
className="clip"
id={this.props.letter}
src={this.props.src}
ref={ref => (this.audio = ref)}
></audio>
</div>
);}
Here's how it looks as a functional component:
let audio = useRef(null);
const handleKeyPress = (event) => {
if (event.keyCode === props.letter.charCodeAt()) {
audio.play();
audio.currentTime = 0;
props.handleDisplay(props.id);
}
};
return (
<div
className="drum-pad"
id={props.id}
onClick={handleClick}
onKeyPress={handleKeyPress}
>
<h1>{props.letter}</h1>
<h2>{props.id}</h2>
<audio
className="clip"
id={props.letter}
src={props.src}
ref={(ref) => (audio = ref)}
></audio>
</div>);
I've tried using useCallback, as well as creating state for audio, but I can't get it to work. When the page is loaded, the first key press plays a sound, but any presses after that, and I get the error:
Uncaught TypeError: Cannot read property 'play' of null
at HTMLDocument.handleKeyPress
Through testing for errors using console.log, it seems that, whenever a key is pressed, the handleKeyPress function is being called 9 times (once for every drum pad). How can I make it so only one specific drum pad gets activated on a single key press, and how can I make sure that audio doesn't revert to null? The issue may also be related to my use of ref. I'm doing this to learn Hooks (and React in general), so any pointers in the right direction would be much appreciated.
SOLUTION
Here is my solution based off of the chosen answer from Oleksandr Kovpashko:
const audio = useRef(null);
const handleKeyPress = useCallback((event) => {
if (event.keyCode === props.letter.charCodeAt()) {
audio.current.play();
audio.current.currentTime = 0;
props.handleDisplay(props.id);
}}, []);
<audio
className="clip"
id={props.letter}
src={props.src}
ref={audio}
></audio>
Upvotes: 0
Views: 329
Reputation: 3274
I see 3 problems:
When you create a ref object using useRef, you can access it by adding .current (for example: audio.current.play())
The charCodeAt function requires an index, maybe this is why handleKeyPress is called several times
Do not call removeEventListener unless you called addEventListener before
Upvotes: 0
Reputation: 800
Seems like you use refs in the wrong way.
The useRef
hook returns a "ref" object that you should pass directly to the ref
prop of the <audio>
tag. Then access the audio using audio.current
in handleKeyPress
function.
Also, you should wrap the event handler into useCallback
hook because the current implementation gives you a new handleKeyPress
function on each render cycle.
PS: Why are you trying to remove the event listener in the returned value from your handleKeyPress
function? It's not necessary to manage event listeners manually with React unless you added them manually using addEventListener
.
Upvotes: 1