Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does changing state (useState) break my p5.js code? (React + p5.js application)

I created an Audio visualizer using p5.js and React and i need some help.

I am trying to change the state of my button input text from Play to Stop when the song starts playing & from Stop back to Play when the user decides to stop the song at that moment.

I am using react-p5 library and everything works great until i setState when a user clicks the play button. Since that causes a re render it breaks my canvas.

Is there a way to only re-render the button element? I am not too sure how i would go about changing the inner text of my button without a state change?

import React, { useState } from 'react';
import Sketch from 'react-p5';
import 'p5/lib/addons/p5.sound';
import Beat from '../../instrumental.mp3';

const App = () => {

  let width = 900;
  let height = 600;
  let song;
  let fft;

  const [flag, setFlag] = useState(true);

  const preload = (p) => {
    p.soundFormats('mp3');
    song = p.loadSound(Beat);
  }

  const setup = (p, canvasParentRef) => {
    // react-p5 conveniently initializes window.p5 thats why all the other p5's
    // have been changed to p in order to create an FFT object
    fft = new p5.FFT();
    // console.log(p.point)
    // use parent to render the canvas in this ref
        // without it p5 will render the canvas outside of this component
    const canvas = p.createCanvas(width, height).parent(canvasParentRef);
  }

  const playStop = () => {
    if (song.isPlaying()) {
      song.pause();
      setFlag(true);
      //p.noLoop();
    } else {
      song.play();
      setFlag(false);
      //p.loop();
    }
  }

  const draw = p => {
    p.background(0);
    // by default the stroke color is black
    // we need to change this in order to see the wave
    p.stroke(255, 204, 0);

    // no fill in between waves
    p.noFill();
    // returns an array with 1024 elements
    let wave = fft.waveform();

    p.beginShape();
    // By looping through the waveform data, we are able
    // to draw the waveform across the canvas
    for (let i = 0; i < width; i++) {
      // create an index that maps the for loop variable
      // to the index of the wave we want
      // value must be integer thats we we use floor
      let index = p.floor(p.map(i, 0, width, 0, wave.length));

      let x = i;
      let y = wave[index] * 100 + height / 2;
      p.vertex(x, y);
    }
    p.endShape();
  }

  return (
    <div className='outerbox'>
      <h1>Audio Visualizer</h1>
      <Sketch preload={preload} setup={setup} draw={draw}/>
      {flag ? <button onClick={playStop}>Play</button> : <button onClick={playStop}>Stop</button>}
    </div>
  );
}

export default App;

The sad thing is there aren't many resources available that includes react + p5.js

If anyone would like to take the time and clone this repository down in order to see what the problem might be, i would very much appreciate that.

Repo link: https://github.com/imperium11/audio-visualizer

  1. npm i
  2. npm run dev-build
  3. npm start
like image 261
Poyraz Akay Avatar asked Oct 20 '25 05:10

Poyraz Akay


1 Answers

The issue here is that each time you update state in a functional component the function gets called again. As a result each time state changes you re-declare your preload/setup/draw, because of the way react-p5 work, the running sketch will start using your updated draw function. However, the updated draw function expects fft to be defined, but the version of the fft variable referenced by the new draw function is undefined.

In order to fix this you can make any local variables that you sketch uses into state variables. In this example I've packed all the locals into one object:

const { useState } = React;
const Sketch = reactP5;

let v = 0;

const App = () => {
  const width = 500;
  const height = 300;

  const [flag, setFlag] = useState(true);
  const [locals, setLocals] = useState({});

  const preload = (p) => {
    p.soundFormats('mp3');
    setLocals({
      song: p.loadSound('https://www.paulwheeler.us/files/Ipu.wav')
    });
  }

  const setup = (p, canvasParentRef) => {
    // react-p5 conveniently initializes window.p5 thats why all the other p5's
    // have been changed to p in order to create an FFT object
    setLocals({
      ...locals,
      fft: new p5.FFT()
    });
    // console.log(p.point)
    // use parent to render the canvas in this ref
        // without it p5 will render the canvas outside of this component
    p.createCanvas(width, height).parent(canvasParentRef);
  };
  
  setup.version = v++;

  const playStop = () => {
    if (locals.song.isPlaying()) {
      locals.song.pause();
      setFlag(true);
      //p.noLoop();
    } else {
      locals.song.play();
      setFlag(false);
      //p.loop();
    }
  }

  const draw = p => {
    p.background(0);
    p.text(setup.version.toString(), 20, 20);
    // by default the stroke color is black
    // we need to change this in order to see the wave
    p.stroke(255, 204, 0);

    // no fill in between waves
    p.noFill();
    // returns an array with 1024 elements
    let wave = locals.fft.waveform();

    p.beginShape();
    // By looping through the waveform data, we are able
    // to draw the waveform across the canvas
    for (let i = 0; i < width; i++) {
      // create an index that maps the for loop variable
      // to the index of the wave we want
      // value must be integer thats we we use floor
      let index = p.floor(p.map(i, 0, width, 0, wave.length));

      let x = i;
      let y = wave[index] * 100 + height / 2;
      p.vertex(x, y);
    }
    p.endShape();
  }

  return (
    <div className='outerbox'>
      <span>Audio Visualizer</span>
      {flag ? <button onClick={playStop}>Play</button> : <button onClick={playStop}>Stop</button>}
      <Sketch preload={preload} setup={setup} draw={draw}/>
    </div>
  );
}

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/index.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/addons/p5.sound.min.js"></script>

<div id="root"></div>

Another way to go would be to actually make the functions that use those local variables into state variables. That way they would only be declared the first time your component function is called for each instance of it. This may be a bit of a hack, but it has the advantage of working for rapidly updating local variables (if you were changing local variables in your draw() function, my understanding is that you would not want to make those into state variables, since high frequency state updates may impact performance.

const { useState } = React;
const Sketch = reactP5;

const App = () => {
  const width = 500;
  const height = 300;
  let song;
  let fft;

  const [flag, setFlag] = useState(true);
  
  const [sketchFns] = useState({
    preload: (p) => {
      p.soundFormats('mp3');
      song = p.loadSound('https://www.paulwheeler.us/files/Ipu.wav');
    },
    setup: (p, canvasParentRef) => {
      // react-p5 conveniently initializes window.p5 thats why all the other p5's
      // have been changed to p in order to create an FFT object
      fft = new p5.FFT();
      // console.log(p.point)
      // use parent to render the canvas in this ref
        // without it p5 will render the canvas outside of this component
      p.createCanvas(width, height).parent(canvasParentRef);
    },
    playStop: () => {
      if (song.isPlaying()) {
        song.pause();
        setFlag(true);
        //p.noLoop();
      } else {
        song.play();
        setFlag(false);
        //p.loop();
      }
    },
    draw: p => {
      p.background(0);
      // by default the stroke color is black
      // we need to change this in order to see the wave
      p.stroke(255, 204, 0);

      // no fill in between waves
      p.noFill();
      // returns an array with 1024 elements
      let wave = fft.waveform();

      p.beginShape();
      // By looping through the waveform data, we are able
      // to draw the waveform across the canvas
      for (let i = 0; i < width; i++) {
        // create an index that maps the for loop variable
        // to the index of the wave we want
        // value must be integer thats we we use floor
        let index = p.floor(p.map(i, 0, width, 0, wave.length));

        let x = i;
        let y = wave[index] * 100 + height / 2;
        p.vertex(x, y);
      }
      p.endShape();
    }
  });

  return (
    <div className='outerbox'>
      <span>Audio Visualizer</span>
      {flag ? <button onClick={sketchFns.playStop}>Play</button> : <button onClick={sketchFns.playStop}>Stop</button>}
      <Sketch preload={sketchFns.preload} setup={sketchFns.setup} draw={sketchFns.draw}/>
    </div>
  );
}

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/index.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/addons/p5.sound.min.js"></script>

<div id="root"></div>
like image 121
Paul Wheeler Avatar answered Oct 21 '25 19:10

Paul Wheeler



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!