Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Component Mounting Twice

Inside a small portion of my React/Redux/ReactRouterV4 application, I have the following component hierarchy,

- Exhibit (Parent)
-- ExhibitOne
-- ExhibitTwo
-- ExhibitThree

Within the children of Exhibit, there are about 6 different possible routes that can be rendered as well. Don't worry, I will explain with some code.

Here is my Parent Exhibit Component:

export class Exhibit extends Component {
  render() {
    const { match, backgroundImage } = this.props

    return (
      <div className="exhibit">
        <Header />
        <SecondaryHeader />

        <div className="journey"
          style={{
            color: 'white',
            backgroundImage: `url(${backgroundImage})`,
            backgroundSize: 'cover',
            backgroundRepeat: 'no-repeat',
            backgroundPosition: 'center-center'
          }}>

          <Switch>
            <Route path={`${match.url}/exhibit-one`} component={ExhibitOne} />
            <Route path={`${match.url}/exhibit-two`} component={ExhibitTwo} />
            <Route path={`${match.url}/exhibit-three`} component={ExhibitThree} />
            <Redirect to="/" />
          </Switch>
        </div>
      </div>
    )
  }
}

Basically, all its does for its job is to display one of the exhibits subcomponents, and set a background image.

Here is one of the subcomponents, ExhibitOne:

export default class ExhibitOne extends Component {
  constructor(props) {
    super(props)
  }

  render() {
    const { match } = this.props

    return (
      <div className="exhibit-one">
        <Switch>
          <Route path={`${match.url}/wall-one`} component={ExhibitHOC(WallOne)} />
          <Route path={`${match.url}/wall-two`} component={ExhibitHOC(WallTwo)} />
          <Route path={`${match.url}/wall-three`} component={ExhibitHOC(WallThree)} />
          <Route path={`${match.url}/wall-four`} component={ExhibitHOC(WallFour)} />
          <Route path={`${match.url}/wall-five`} component={ExhibitHOC(WallFive)} />
          <Route path={`${match.url}/wall-six`} component={ExhibitHOC(WallSix)} />
        </Switch>
      </div>
    )
  }
}

In order to cut down on typing, I decided to wrap the components in a Higher Order Component, whose purpose is to dispatch an action that will set the proper background image on the top level Exhibit parent component.

This is the Higher Order Component:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions/wall-background-image'

export default function(ComposedComponent) {
  class ExhibitHoc extends Component {

    componentDidMount = () => this.props.setBackgroundImage(`./img/exhibit-one/${this.getWall()}/bg.jpg`)

    getWall = () => {
      // this part isnt important. it is a function that determines what wall I am on, in order to set
      // the proper image.
    }

    render() {
      return <ComposedComponent />
    }
  }

  return connect(null, actions)(ExhibitHoc);
}

On initial load of ExhibitOne, I can see that the setBackgroundImage action creator executes twice by looking at Redux Logger in the console. My initial inclination to use componentDidMount was because I thought using it would limit the action creator to execute only once. Here is a screenshot of the log:

enter image description here

I think I might be misunderstanding how Higher Order Components work, or maybe its some type of React Router V4 thing? Anyways, any help would be greatly appreciated as to why this executes twice.

like image 766
Dan Zuzevich Avatar asked Mar 01 '18 17:03

Dan Zuzevich


2 Answers

Here in 2020, this was being caused by <React.StrictMode> component that was wrapped around the <App /> in new versions of Create React App. Removing the offending component from index.js fixed the double mount problem for all of my components. I don't know if this was by design or what, but it was annoying and misleading to see console.logs() twice for everything.

like image 177
SacWebDeveloper Avatar answered Sep 28 '22 10:09

SacWebDeveloper


The problem is that the component prop here is a function application, which yields a new class on each render. This will cause the previous component to unmount and the new one to mount (see the docs for react-router for more information). Normally you would use the render prop to handle this, but this won't work with higher-order components, as any component that is created with a HOC application during rendering will get remounted during React's reconciliation anyway.

A simple solution is to create your components outside the ExhibitOne class, e.g.:

const ExhibitWallOne = ExhibitHOC(WallOne);
const ExhibitWallTwo = ExhibitHOC(WallTwo);
..
export default class ExhibitOne extends Component {
  ..
          <Route path={`${match.url}/wall-one`} component={ExhibitWallOne} />
          <Route path={`${match.url}/wall-two`} component={ExhibitWallTwo} />
          ..
}

Alternatively, depending on what the wrapper does, it might be possible to declare it as a normal component that renders {this.props.children} instead of the parameter <ComposedComponent/>, and wrap the components in each Route:

<Route path={`${match.url}/wall-one`}
       render={(props) => <Wrap><WallOne {...props}/></Wrap>}
/>

Note that you'll need to use render instead of component to prevent remounting. If the components don't use routing props, you could even remove {...props}.

like image 20
Oblosys Avatar answered Sep 28 '22 10:09

Oblosys