Cannot update a component (`App`) while rendering a different component

11,822

The error message will be displayed if parent (App) states are set while rendering children (Knob).

In your case, while App is rendering, Knob'sonChange() is triggered when loaded, which then calls this.handleOnChange() and then this.props.handleChangePan() having App'ssetPanLevel().

To fix using useEffect():

  1. In knob.js, you can store panLevel as state first just like in App, instead of direct calling this.props.handleChangePan() to call App'ssetPanLevel().
  2. Then, use useEffect(_=>props.handleChangePan(panLevel),[panLevel]) to call App'ssetPanLevel() via useEffect().

Your knob.js will look like this:

function Knob(props){
  let [panLevel, setPanLevel] = useState(50);
  useEffect(_=>{
    props.handleChangePan(panLevel);
  }, [panLevel]);
  return *** Knob that does not call props.handleChangePan(), but call setPanLevel() instead ***;
}

setState() called inside useEffect() will be effective after the render is done.

In short, you cannot call parent'ssetState() outside useEffect() while in first rendering, or the error message will come up.

Share:
11,822
skavan
Author by

skavan

Updated on July 18, 2022

Comments

  • skavan
    skavan over 1 year

    There are a bunch of similar questions on so, but I can't see one that matches my conundrum.

    I have a react component (a radial knob control - kinda like a slider). I want to achieve two outcomes:

    1. Twiddle the knob and pass the knob value up to the parent for further actions.
    2. Receive a target knob value from the parent and update the knob accordingly.
    3. All without going into an endless loop!

    I have pulled my hair out - but have a working solution that seems to violate react principles.

    I have knob.js as a react component that wraps around the third party knob component and I have app.js as the parent.

    In knob.js, we have:

    export default class MyKnob extends React.Component {
        constructor(props, context) {
            super(props, context)
    
            this.state = {
                size: props.size || 100,
                radius: (props.value/2).toString(),
                fontSize: (props.size * .2)
            }
            if (props.value){
                console.log("setting value prop", props.value)
                this.state.value = props.value
            } else {
                this.state.value = 25           // any old default value
            }
    
          }
    

    To handle updates from the parent (app.js) I have this in knob.js:

          // this is required to allow changes at the parent to update the knob
          componentDidUpdate(prevProps) {
            if (prevProps.value !== this.props.value) {
               this.setState({value: this.props.value})
            }
            console.log("updating knob from parent", value)
          }
    

    and then to pass changes in knob value back to the parent, I have:

        handleOnChange = (e)=>{
            //this.setState({value: e})    <--used to be required until line below inserted. 
            this.props.handleChangePan(e)
          }
    

    This also works but triggers a warning:

    Cannot update a component (App) while rendering a different component (Knob)

    render(){
            return (
                <Styles font-size={this.state.fontSize}>
                <Knob size={this.state.size}  
                    angleOffset={220} 
                    angleRange={280}
                    steps={10}
                    min={0}
                    max={100}
                    value={this.state.value}
                    ref={this.ref}
                    onChange={value => this.handleOnChange(value)}
                >
    ...
    

    Now over to app.js:

    function App() {
      const [panLevel, setPanLevel] = useState(50);
    
    // called by the child knob component. works -- but creates the warning
        function handleChangePan(e){
          setPanLevel(e)
        }
    
        // helper function for testing
        function changePan(e){
          if (panLevel + 10>100){
            setPanLevel(0)
          } else {
            setPanLevel(panLevel+10)
          }
        }
    
    return (
        <div className="App">
            ....
            <div className='mixer'>
              <div key={1} className='vStrip'>
                <Knob size={150} value={panLevel} handleChangePan = {(e) => handleChangePan(e)}/>
              </div>
            <button onClick={(e) => changePan(e)}>CLICK ME TO INCREMENT BY 10</button>
          ...
        </div>
    

    So - it works -- but I am violating react principles -- I haven't found another way to keep the external "knob value" and the internal "knob value" in sync.

    Just to mess with my head further, if I remove the bubbling to parent in 'handleOnChange' - which presumably then triggers a change in prop-->state cascading back down - I not only have a lack of sync with the parent -- but I also need to reinstate the setState below, in order to get the knob to work via twiddling (mouse etc.._)! This creates another warning:

    Update during an existing state transition...

    So stuck. Advice requested and gratefully received. Apols for the long post.

        handleOnChange = (e)=>{
            //this.setState({value: e})
            **this.props.handleChangePan(e)**
          }
    

    It has been suggested on another post, that one should wrap the setState into a useEffect - but I can't figure out how to do that - let alone whether it's the right approach.