Reputation: 417
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.
Upvotes: 10
Views: 19807
Reputation: 555
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.
Upvotes: 18