I'm struggling to pose this question in a concise manner. I am having some major performance issues with my app. I have installed the Perf add-on tools for react and can see where the issue is, however I am unsure of the best way to fix it.
I think it will probably have something to do with ReSelect... but need some guidance on where to begin.
I have a component that renders a number of other components. This uses size-me (to calculate the size of the browsing window), and react-grid-layout (to layout each component and permit their positioning to be changed). This is resource intensive, so I can't have this happening unnecessarily.
The user can click on a button to open a modal window (to add or edit the components that are being rendered in the grid).
The issue: When the modal window opens, the underlying component re-renders, causing size-me and react-grid-layout to re-render, which thus causes the modal to "jerkingly" open!
This is the entire state tree:
This is the only part of the state that changes when I open the modal:
The size-me and react-grid-layout stuff is rendering state from the formEngine.form part of the state tree, yet it is being re-rendered when state updates are made to the formEngine.addComponent part of the tree
Here are the performance logs:
As you can see, there are some wasted renders happening AND this will only grow incrementally based on the number of nested layout components the user decides to add to the form...
So to try and prevent this question from becoming too convoluted, let me ask first:
Thank you.
EDIT 1:
I'm not sure if this is relevant, but to answer the comment, I added this code. The AddFormComponent is the Modal that jerks open.
Form.js:
const Form = (props) => (
<div className="form-engine">
<div className="card-block" style={{position: "relative"}}>
{
props.editMode &&
<div className="nula-form-controls">
<AddFormComponent parentId={"root"} />
</div>
}
{
props.form.components.root.childComponentIds.length > 0 ?
<LayoutComponent componentKey={"root"} />
:
<EmptyGridLayout />
}
</div>
</div>
)
LayoutComponent.js:
import React from 'react'
import _ from 'lodash'
import SizeMe from 'react-sizeme'
import { Responsive as ResponsiveReactGridLayout } from 'react-grid-layout'
import 'react-grid-layout/css/styles.css'
import 'react-resizable/css/styles.css'
import FormComponent from '../containers/FormComponent'
import NestedLayoutComponent from '../containers/LayoutComponent'
import AddFormComponent from '../containers/AddFormComponent'
import LayoutComponentEditor from '../containers/LayoutComponentEditor'
//Setup SizeMe Configuration
let sizeMeConfig = {
monitorWidth: true
}
let sizeMeHOC = SizeMe(sizeMeConfig)
//Wrap ResponsiveReactGridLayout in sizeMeHOC so that it is aware of it's width
var GridLayout = ResponsiveReactGridLayout
GridLayout = sizeMeHOC(GridLayout)
const LayoutComponent = (props) => (
<div>
<GridLayout
cols={props.cols}
className={props.className}
breakpoints={props.breakpoints}
rowHeight={props.rowHeight}
draggableCancel={props.draggableCancel}
layouts={props.layouts}
isDraggable={props.isDraggable}
isResizable={props.isResizable}
onLayoutChange={(currentLayout, allLayouts) => props.handleLayoutChange(props.componentKey, currentLayout, allLayouts)}
width={props.size.width}
>
{
//Map out any child layouts
props.childComponents.map((component) => {
if (component.type === "card") {
return (
<div className={"card card-outline-" + component.color} key={component.key}>
<div className={"card-header card-" + component.color}>
{component.header}
</div>
<div className="card-block" style={{overflowY: "auto", position: "relative"}}>
{
//Hide if editMode={false}
props.editMode &&
<div className="nula-card-controls">
<LayoutComponentEditor path={component.key} />
<a href="#" className="text-danger" title="Remove"><span className="fa fa-trash" /></a>
<AddFormComponent parentId={component.key} />
</div>
}
<NestedLayoutComponent componentKey={component.key} />
</div>
</div>
)
}
else if (component.type === "fieldGroup") {
return (
<div className="card" key={component.key}>
<div className="card-block pl-0 pr-0 pt-2 pb-0" style={{overflowY: "auto"}}>
{
//Hide if editMode={false}
props.editMode &&
<div className="nula-fieldgroup-controls">
<a className="text-warning" title="Edit"><span className="fa fa-pencil" /></a>
<a className="text-danger" title="Remove"><span className="fa fa-trash" /></a>
<AddFormComponent parentId={component.key} />
</div>
}
<NestedLayoutComponent componentKey={component.key} />
</div>
</div>
)
}
else if (component.type === "paragraph") {
return (
<div className="alert alert-success text-font-bold" key={component.key}>
{
<FormComponent component={component} editMode={props.editMode} />
}
</div>
)
}
else {
return (
<div key={component.key}>
{
<FormComponent component={component} editMode={props.editMode} />
}
</div>
)
}
})
}
</GridLayout>
</div>
)
export default SizeMe()(LayoutComponent)
EDIT 2:
AddFormComponent.js -- Component
import React from 'react'
import AddFormComponentDetails from './AddFormComponentDetails'
import Perf from 'react-addons-perf'; // ES6
class AddFormComponent extends React.Component {
constructor(props) {
super(props);
this.localOpenModal = this.localOpenModal.bind(this);
}
localOpenModal() {
console.log("----STARTING PERFORMANCE MONITOR-----")
Perf.start()
this.props.handleOpenModal();
}
componentDidUpdate() {
console.log("-----PERFORMANCE MONITOR STOPPING------")
Perf.stop()
console.log("-----PRINT INCLUSIVE------")
Perf.printInclusive()
console.log("-----PRINT WASTEED------")
Perf.printWasted()
}
render() {
return (
<span>
<a onTouchTap={this.localOpenModal} className="text-success" title="Add Component">
<span className="fa fa-plus" />
</a>
<Modal isOpen={this.props.modalOpen} size={"lgr"} toggle={this.props.handleCloseModal}>
<ModalHeader toggle={this.props.handleCloseModal}>Add Component</ModalHeader>
<ModalBody>
...Removed For Breviety
</ModalBody>
<ModalFooter>
...Removed For Breviety
</ModalFooter>
</Modal>
</span>
)
}
}
export default AddFormComponent
AddFormComponent.js -- Container
import { connect } from 'react-redux'
import {
handleOpenModal,
handleCloseModal,
handleGoBack,
handleComponentPropertyChange,
handleComponentNameChange,
handleComponentTypeChange,
handleSubmit
} from '../actions/addFormComponentActions'
import AddFormComponent from '../components/AddFormComponent'
const mapStateToProps = (state) => ({
steps: [
{ icon: 'superpowers', title: 'Select Component', description: 'Select the Component you wish to add', active: state.addComponent.currentStep == 1 },
{ icon: 'info circle', title: 'Enter Details', description: 'Enter details to customize component', active: state.addComponent.currentStep == 2 },
{ icon: 'check', title: 'Add Component', description: 'Add component to form' }
],
currentStep: state.addComponent.currentStep,
modalOpen: state.addComponent.modalOpen,
component: state.addComponent.component,
errors: state.addComponent.errors,
componentType: state.addComponent.componentType
})
export default connect(
mapStateToProps,
{
handleOpenModal,
handleCloseModal,
handleGoBack,
handleComponentPropertyChange,
handleComponentNameChange,
handleComponentTypeChange,
handleSubmit
}
)(AddFormComponent)
addFormComponentReducer.js
import _ from 'lodash'
import {
ADD_FORM_COMPONENT_TOGGLE_MODAL,
ADD_FORM_COMPONENT_CLOSE_MODAL,
ADD_FORM_COMPONENT_GO_BACK,
ADD_FORM_COMPONENT_SUBMIT,
ADD_FORM_COMPONENT_PROPERTY_CHANGE,
ADD_FORM_COMPONENT_PROPERTY_ERROR,
ADD_FORM_COMPONENT_KEY_ERROR,
ADD_FORM_COMPONENT_NAME_CHANGE,
ADD_FORM_COMPONENT_NAME_ERROR,
ADD_FORM_COMPONENT_TYPE_CHANGE,
ADD_FORM_COMPONENT_TYPE_ERROR
} from '../actions/addFormComponentActions'
let initialState = {
currentStep: 1,
modalOpen: false,
component: {
key: '',
label: '',
headingText: '',
text: ''
},
errors: {
key: {
hasError: false,
msg: ''
},
label: {
hasError: false,
msg: ''
},
text: {
hasError: false,
msg: ''
}
}
}
function addFormComponentReducer(state = initialState, action) {
switch (action.type) {
case ADD_FORM_COMPONENT_TOGGLE_MODAL:
return {
...state,
modalOpen: action.payload.isOpen,
currentStep: 1
}
case ADD_FORM_COMPONENT_CLOSE_MODAL:
return initialState;
case ADD_FORM_COMPONENT_GO_BACK:
return {
...state,
currentStep: 1
}
case ADD_FORM_COMPONENT_SUBMIT:
return initialState;
case ADD_FORM_COMPONENT_PROPERTY_CHANGE:
return {
...state,
component: {
...state.component,
[action.payload.key]: action.payload.value
}
}
case ADD_FORM_COMPONENT_PROPERTY_ERROR:
return {
...state,
errors: {
...state.errors,
[action.payload.key]: {
hasError: action.payload.hasError,
msg: action.payload.msg
}
}
}
case ADD_FORM_COMPONENT_TYPE_CHANGE:
return {
...state,
componentType: action.payload.componentType,
currentStep: 2
}
default:
return state
}
}
export default addFormComponentReducer
index.js -- Combine Reducers
import { combineReducers } from 'redux'
//import FormEngine reducers
import formReducer from './formReducer'
//import addFormComponentReducer from './addFormComponentReducer'
import componentEditorReducer from './componentEditorReducer'
const rootFormEngineReducer = combineReducers({
form: formReducer,
//addComponent: addFormComponentReducer,
componentEditor: componentEditorReducer
})
export default rootFormEngineReducer
rootReducer.js
import { combineReducers } from 'redux'
//import reducers
import rootCoreLayoutReducer from '../features/CoreLayout/reducers'
import rootFormEngineReducer from '../features/FormEngine/reducers'
import addComponentReducer from '../features/FormEngine/reducers/addFormComponentReducer'
const rootReducer = combineReducers({
coreLayout: rootCoreLayoutReducer,
formEngine: rootFormEngineReducer,
addComponent: addComponentReducer
})
export default rootReducer
If you are using a pure component any performance optimizations have to be handled manually(using shouldComponentUpdate). Since you are using redux it can handle that for you. But you have to "connect" it to the redux store.
If you choose to use redux connect ensure that the modal visibility is not related to your other properties specifically in your case:
modalOpen is nested in formEngine. When it changes anything else that listens to formEngine will rerender
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