Home  >  Q&A  >  body text

React subcomponents cannot sense state changes from asynchronous functions inside useEffect

<p>I'm trying to pass a state generated inside an async function, which itself is inside useState. I learned that useRef is probably the best way to solve this problem because it can reference mutable state, and in the process learned about React-useStateRef, which finally solved another problem where the state inside my main component was never updated Problem (keeps getting "too many renders" errors). So it essentially acts as useRef and useState rolled into one. </p> <p>However, although my state finally updated, it still wasn't passed to my Canvas component. I'm trying to update a canvas's BG graph based on the temperatures obtained from a dataset. </p> <pre class="brush:php;toolbar:false;">import { useEffect } from 'react'; import useState from 'react-usestateref'; import { getServerData } from './serviceData'; import "./App.css" import Canvas from './components/Canvas'; function App() { const [dataset, setDataset] = useState(null); const [units, setUnits] = useState('metric'); // canvas background graphic stuff var [currentBG, setCurrentBG, currentBGref] = useState(null); useEffect(() => { const fetchServerData = async () => { const data = await getServerData(city, units); setDataset(data); function updateBG() { const threshold = units === "metric" ? 20 : 60; if (data.temp <= threshold) { setCurrentBG('snow'); } else { setCurrentBG('sunny'); } } updateBG(); } fetchServerData(); console.log(currentBG) }, [city, units, currentBGref.current, currentFGref.current]) const isCelsius = currentUnit === "C"; button.innerText = isCelsius ? "°F" : "°C"; setUnits(isCelsius ? "metric" : "imperial"); }; return ( <div className="app"> { dataset && ( <Canvas width={640} height={480} currentBG={currentBGref.current}></Canvas> )} </div> ); } export default App;</pre> <p>I can only pass the initial value and it never updates past that, although the console.log inside useEffect shows that it is definitely updating. So why isn't it being passed to my component? </p>
P粉609866533P粉609866533435 days ago491

reply all(1)I'll reply

  • P粉501007768

    P粉5010077682023-09-03 00:25:16

    useStateRef seems to be an anti-pattern. You decide what the new state is, so if you need another reference to it, you can always create one yourself. I recommend minimizing the properties on the canvas to prevent unnecessary re-rendering.

    function App({ width, height }) {
      const canvas = React.useRef()
      const [color, setColor] = React.useState("white")
    
      // when color changes..
      React.useEffect(() => {
        if (canvas.current) {
          const context = canvas.current.getContext('2d');
          context.fillStyle = color
          context.fillRect(0, 0, width, height)
        }
      }, [color, width, height])
    
      return <div>
        <canvas ref={canvas} width={width} height={height} />
        <button onClick={_ => setColor("blue")} children="blue" />
        <button onClick={_ => setColor("green")} children="green" />
        <button onClick={_ => setColor("red")} children="red" />
      </div>
    }
    
    ReactDOM.createRoot(document.querySelector("#app")).render(<App width={200} height={150} />)
    canvas { display: block; border: 1px solid black; }
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <div id="app"></div>

    Or maybe there's no reason to store colors as state. You can create the setColor function yourself and attach it as an event listener -

    function App({ width, height }) {
      const canvas = React.useRef()
      const setColor = color => event => {
        if (canvas.current) {
          const context = canvas.current.getContext('2d');
          context.fillStyle = color
          context.fillRect(0, 0, width, height)
        }
      }
      return <div>
        <canvas ref={canvas} width={width} height={height} />
        <button onClick={setColor("blue")} children="blue" />
        <button onClick={setColor("green")} children="green" />
        <button onClick={setColor("red")} children="red" />
      </div>
    }
    
    ReactDOM.createRoot(document.querySelector("#app")).render(<App width={200} height={150} />)
    canvas { display: block; border: 1px solid black; }
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <div id="app"></div>

    I also recommend checking out SVG. I find that the SVG API fits the React pattern better than the Canvas API. The capabilities of each are different, but if SVG works for your needs, it's worth considering.

    reply
    0
  • Cancelreply