Reputation: 2517
I have the following scenario:
1) There is a parent component "ModuleListContainer".
2) A module (in the module list, also a child component, but not interesting in this context) gets selected when hovering over it a module item in the list.
3) When hovering over a module, a menu should be shown in the corner of the module.
4) The whole parent component should NOT be updated when a module is selected, since it can be quite a long list of modules, that is why I set shouldComponentUpdate = false
when updating which module should be selected.
5) The menu is loaded when the parent component loads, and only its position is updated when hovering over a module.
This is the parent component (simplified)...
class ModuleListContainer extends Component {
constructor(props) {
super(props);
this.state = {
selectingModule: false,
currentlySelectedModule: nextProps.currentModule
}
}
shouldComponentUpdate(nextProps, nextState) {
if (nextState.selectingModule === true) {
this.setState({
selectingModule: false,
currentlySelectedModule: null
})
return false;
}
return true;
}
mouseEnterModule = (e, moduleItem) => {
const menu = document.getElementById('StickyMenu');
const menuPosition = calculateModuleMenuPosition(e.currentTarget);
if (moduleItem.ModuleId !== this.props.currentModuleId) {
this.props.actions.selectModule(moduleItem);
this.setState({
selectingModule: true
});
}
menu.style.top = menuPosition.topPos + 'px';
menu.style.left = menuPosition.leftPos + 'px';
}
render() {
return (
<div>
<section id="module-listing">
{/* ... list of mapped modules with mouseEnterModule event */}
</section>
<ModuleMenu {... this.props} currentlySelectedModule={this.state.currentlySelectedModule} />
</div>
);
}
}
This is the menu component (simplified)...
class ModuleMenu extends Component {
constructor(props) {
super(props);
this.state = {
currentModule: this.props.currentlySelectedModule
};
}
clickMenuButton = () => {
console.log('CURRENT MODULE', this.state.currentModule);
}
render() {
return (
<div id="StickyMenu">
<button type="button" onClick={this.clickMenuButton}>
<span className="fa fa-pencil"></span>
</button>
</div>
);
}
}
When, in my menu component, I try to console.log
the current module from the state, I keep getting null
.
My question is if this is because...
I have set the shouldComponentUpdate to false and the menu's state does not get updated?
Or could it be because I do not re-render the whole component?
Or is it because I load the menu together with the parent component and it does not get re-rendered when a module is selected?
Or is it possibly a combination of some of the above?
The react docs (https://reactjs.org/docs/react-component.html) says:
Returning false does not prevent child components from re-rendering when their state changes.
Therefore, I am hoping that it is none of the above since I really don't want to have to re-render the entire component when selecting a module.
Upvotes: 1
Views: 1460
Reputation: 1754
I think that in your code there are other problems you need to solve before looking for a practical solution, starting from the use you make of shouldComponentUpdate()
.
Official doc says that:
Use shouldComponentUpdate() to let React know if a component’s output is not affected by the current change in state or props. The default behavior is to re-render on every state change, and in the vast majority of cases you should rely on the default behavior. shouldComponentUpdate() is invoked before rendering when new props or state are being received. Defaults to true. This method is not called for the initial render or when forceUpdate() is used.
If you perform a setState()
call inside the shouldComponentUpdate()
function it might work but essentially you are telling React to start a new render cycle before knowing if in this execution it should render or not.
Also keep in mind that setState()
is not guaranteed to be executed immediately:
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
Moreover (not very clear from the code, so I guess) you are separating Component and DOM object for ModuleMenu
: its representation must be guided by state or props, here instead you are using HTMLElement.style.x syntax to set its representation properties.
I'd restructure ModuleListContainer
to store in its state an object that represents ModuleMenu properties, that will be passed to ModuleMenu component as props, something like this:
moduleMenu {
currentModuleId: ... ,
top: ... ,
left: ...
}
And set the state in mouseEnterModule
handler:
mouseEnterModule = (e, moduleItem) => {
const menuPosition = calculateModuleMenuPosition(e.currentTarget);
if (moduleItem.ModuleId !== this.props.currentModuleId) {
this.props.actions.selectModule(moduleItem);
this.setState({
moduleMenu: {
currentModuleId: moduleItem.ModuleId,
left: menuPosition.leftPos + 'px',
top: menuPosition.topPos + 'px'
}
});
}
}
Then ModuleMenu can get the new position like this:
<div id="StickyMenu">
style={{
left: this.props.left,
top: this.props.top
}}
>
...
</div>
Of course you can still use shouldComponentUpdate()
to determine which modules in the list should be updated but this time returning just a boolean after a comparison of (once again, I guess) ids of items; avoiding too many calls to setState()
method.
Hope this may help you!
Upvotes: 1
Reputation: 9944
Your children state doesn't change in this case, you're only changing the state of the parent. What you should probably do is split the render method of your component into two components:
render() {
return (
<div>
<NoUpdateComponent someProps={this.props.someProps}/>
<ModuleMenu {... this.props} currentlySelectedModule={this.state.currentlySelectedModule} />
</div>
);
}
And then in your first costly component, use the shouldComponentUpdate
method to prevent it from re rendering
Upvotes: 1