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:
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.
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.
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.
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.
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.
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?
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:
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.
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.
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()
:
knob.js
, you can store panLevel
as state first just like in App, instead of direct calling this.props.handleChangePan()
to call App'ssetPanLevel()
.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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With