Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 346
skavan Avatar asked Jun 06 '20 18:06

skavan


People also ask

How do you fix Cannot update a component while rendering a different component?

To fix cannot update a component while rendering a different component warning with React, we should call functions from props inside the useEffect callback or in functions in the component.

How do you update components to another?

You don't update a component from another. Instead: components render from a shared top level data model. Callbacks are passed down to components. Any one of them can trigger a data change on that data model through the callbacks.

Can I setState In render?

render() Calling setState() here makes it possible for a component to produce infinite loops. The render() function should be pure, meaning that it does not modify a component's state. It returns the same result each time it's invoked, and it does not directly interact with the browser.

What is a react memo?

React. memo is a higher order component. If your component renders the same result given the same props, you can wrap it in a call to React. memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.

How to fix cannot update a component while rendering a different component?

To fix cannot update a component while rendering a different component warning with React, we should call functions from props inside the useEffect callback or in functions in the component. ← How to handle specific errors in JavaScript? → How to fix Phantom.js not waiting for full page load with JavaScript?

What is the react function component Update warning?

React 16.13.0 introduced a warning for when a function component is updated during another component's render phase ( facebook/react#17099 ). In version 16.13.1 the warning was adjusted to be more specific ( facebook/react#18330 ). The warning looks like this:

Can a function component queue a state while rendering?

A function component is allowed to queue a state update, while rendering, for itself only. As my example showed, that acts as the equivalent of getDerivedStateFromProps. Queueing an update for any other component from within the actual rendering body of a function component is illegal.

Is the function component body the same as the render method?

That function component body is essentially the same thing as class component render method. It is indeed our omission that setState on another component during the function component body did not warn before. You could infer it’s a bad pattern from the two points above but it’s fair to say one could not realize it.


1 Answers

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.

like image 163
Northnroro Avatar answered Oct 15 '22 22:10

Northnroro