最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - React hooks - how to force useEffect to run when state changes to the same value? - Stack Overflow

programmeradmin2浏览0评论

So I'm building a drum-pad type of app, and almost everything is working, except this.

Edit: Put the whole thing on codesandbox, if anyone wants to have a look: codesandbox.io/s/sleepy-darwin-jc9b5?file=/src/App.js

const [index, setIndex] = useState(0);
const [text, setText] = useState("");
const [theSound] = useSound(drumBank[index].url)   

function playThis(num) {
  setIndex(num)
}

useEffect(()=>{
  if (index !== null) {
    setText(drumBank[index].id);
    theSound(index);
    console.log(index)
  }
}, [index])

When I press a button, the index changes to the value associated with the button and then the useEffect hook plays the sound from an array at that index. However, when I press the same button more than once, it only plays once, because useState doesn't re-render the app when the index is changed to the same value.

I want to be able to press the same button multiple times and get the app to re-render, and therefore useEffect to run every time I press the button. Can anyone help me how to do this?

So I'm building a drum-pad type of app, and almost everything is working, except this.

Edit: Put the whole thing on codesandbox, if anyone wants to have a look: codesandbox.io/s/sleepy-darwin-jc9b5?file=/src/App.js

const [index, setIndex] = useState(0);
const [text, setText] = useState("");
const [theSound] = useSound(drumBank[index].url)   

function playThis(num) {
  setIndex(num)
}

useEffect(()=>{
  if (index !== null) {
    setText(drumBank[index].id);
    theSound(index);
    console.log(index)
  }
}, [index])

When I press a button, the index changes to the value associated with the button and then the useEffect hook plays the sound from an array at that index. However, when I press the same button more than once, it only plays once, because useState doesn't re-render the app when the index is changed to the same value.

I want to be able to press the same button multiple times and get the app to re-render, and therefore useEffect to run every time I press the button. Can anyone help me how to do this?

Share Improve this question edited Mar 10, 2021 at 10:12 Lursmani asked Mar 10, 2021 at 7:48 LursmaniLursmani 1691 gold badge1 silver badge12 bronze badges 6
  • I found this custom hooks for key press may be that can help you useKeyPress – antoineso Commented Mar 10, 2021 at 8:05
  • Why use an useEffect hook at all for this and not just associate a keyPress event listener callback with a specific index value to pass to theSound? Perhaps I'm missing some context in this minimal example? The useEffect is intended to run an effect when a dependency changes, not when an asynchronous event happens. It doesn't seem to be the correct tool for the job. – Drew Reese Commented Mar 10, 2021 at 8:09
  • @DrewReese is right, why use useEffect when it's not beneficial to you whatsoever. Just wrap all in one callback playThis is enough. – vuongvu Commented Mar 10, 2021 at 8:20
  • @DrewReese I tried that at first, putting everything within playThis(), but useSound is a custom hook that can not be inside a normal function. That's why I have it set to play the sound at index and not num. And if I put it in its current form within playThis, it lags behind and plays the sound at the previous index because state is asynchronous. – Lursmani Commented Mar 10, 2021 at 8:47
  • You don't need to put useSound in any callback, and for other than obvious hook reasons, but theSound can. I think a React ref may help you here. Do you have the ability to create a running codesandbox with this code mostly working? I have an idea but I am just not certain where the text state or drumBank e into play. – Drew Reese Commented Mar 10, 2021 at 9:01
 |  Show 1 more ment

2 Answers 2

Reset to default 2

Here's what I could e up with from your sandbox.

  1. According to the docs each useSound is just a single sound, so when trying to update an index into a soundbank to use via React state the sound played will always be at least one render cycle delayed. I suggest creating a new custom hook to encapsulate your 9 drum sounds.

    useDrumBank consumes the drumbank array and instantiates the 9 drum sounds into an array.

    const useDrumBank = (drumbank) => {
      const [drum0] = useSound(drumbank[0].url);
      const [drum1] = useSound(drumbank[1].url);
      const [drum2] = useSound(drumbank[2].url);
      const [drum3] = useSound(drumbank[3].url);
      const [drum4] = useSound(drumbank[4].url);
      const [drum5] = useSound(drumbank[5].url);
      const [drum6] = useSound(drumbank[6].url);
      const [drum7] = useSound(drumbank[7].url);
      const [drum8] = useSound(drumbank[8].url);
    
      return [drum0, drum1, drum2, drum3, drum4, drum5, drum6, drum7, drum8];
    };
    
  2. Update the ponent logic to pass the drumBank array to the new custom hook.

    const sounds = useDrumBank(drumBank);
    

Here's the full code:

function App() {
  useEffect(() => {
    document.addEventListener("keypress", key);

    return () => document.removeEventListener("keypress", key);
  }, []);

  const [text, setText] = useState("");
  const sounds = useDrumBank(drumBank);

  function playThis(index) {
    drumBank[index]?.id && setText(drumBank[index].id);
    sounds[index]();
  }

  function key(e) {
    const index = drumBank.findIndex((drum) => drum.keyTrigger === e.key);
    index !== -1 && playThis(index);
  }

  return (
    <div id="drum-machine" className="drumpad-container">
      <div id="display" className="drumpad-display">
        <p>{text}</p>
      </div>
      <button className="drum-pad" id="drum-pad-1" onClick={() => playThis(0)}>
        Q
      </button>
      <button className="drum-pad" id="drum-pad-2" onClick={() => playThis(1)}>
        W
      </button>
      <button className="drum-pad" id="drum-pad-3" onClick={() => playThis(2)}>
        E
      </button>
      <button className="drum-pad" id="drum-pad-4" onClick={() => playThis(3)}>
        A
      </button>
      <button className="drum-pad" id="drum-pad-5" onClick={() => playThis(4)}>
        S
      </button>
      <button className="drum-pad" id="drum-pad-6" onClick={() => playThis(5)}>
        D
      </button>
      <button className="drum-pad" id="drum-pad-7" onClick={() => playThis(6)}>
        Z
      </button>
      <button className="drum-pad" id="drum-pad-8" onClick={() => playThis(7)}>
        X
      </button>
      <button className="drum-pad" id="drum-pad-9" onClick={() => playThis(8)}>
        C
      </button>
    </div>
  );
}

Demo

Usage Notes

No sounds immediately after load

For the user's sake, browsers don't allow websites to produce sound until the user has interacted with them (eg. by clicking on something). No sound will be produced until the user clicks, taps, or triggers something.

Getting the keypresses to consistently work seems to be an issue and I don't immediately have a solution in mind, but at least at this point the button clicks work and the sounds are played synchronously.

Have you considered bining those discrete state variables (value types) into a single reference type state object?

Instead of having an effect that sets the text when the index changes you just set the entire state at the same time.

As long as you ensure this is a new object/reference then the effect will fire. The effect is then only responsible for playing the sound based on the current state.

Here's some sample code

const [state, setState] = useState({ index: 0, text: '', }); 
const playThis = (num) => { 
    setState({ index: num, text: drumBank[num].id, }); 
}; 
useEffect(() => { 
    theSound(state.index); 
}, [state]);
发布评论

评论列表(0)

  1. 暂无评论