React-Redux - How to update a piece of state without causing unnecessary re-renders on unrelated components
javascript, react-redux, reactjs, redux
Solution
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
Problem
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: - How do I prevent the underlying page from re-rendering when I open the modal? - Why are components that are watching formEngine.form triggered to re-render when fromEngine.addComponent gets modified? 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 ```