19

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.

Dan Zuzevich
  • 3,651
  • 3
  • 26
  • 39

4 Answers4

69

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. This was by design, but it was annoying and misleading to see console.log() twice for everything.

For Next.js, in next.config.js, set reactStrictMode:false.

SacWebDeveloper
  • 2,563
  • 2
  • 18
  • 15
  • Good catch. I hadn't noticed that before. File a bug report maybe? – Dara Java Oct 21 '20 at 03:34
  • 1
    Brilliant! Saved me a couple hours of head scratching! It makes sense to me that StrictMode would function this way, so I doubt it could be considered a bug. However, the double mounting is so unexpected, especially when you aren't aware `` is even there... it might be nice to see an output in the console indicating what's going on. – Rolandus Apr 13 '21 at 19:53
  • 1
    I spent an entire day debugging my app thinking there was something I was doing wrong. This fixed it (react version 17.0.2)npm. Thank you. – Jon Doe Aug 08 '21 at 19:33
  • 5
    According to this, it is supposed to mount twice (only in dev, not in prod): https://stackoverflow.com/a/61897567/1778068 – marvin Apr 27 '22 at 18:40
  • @marvin Thank you! I use NextJS and I had reactStrictMode: true, which causes each component to mount twice (on purpose, in dev, for debugging help.. not needed). I set next.config.js reactStrictMode: false and now my components load once, as expected. – Marty McGee Aug 27 '22 at 04:27
20

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}.

Oblosys
  • 14,468
  • 3
  • 30
  • 38
1

If you use 'Hidden Material UI React', it mounts your component every time you call it. For example, I wrote the below one:

<Hidden mdDown implementation="css">
    <Container component="main" maxWidth="sm">
        {content}
    </Container>
</Hidden>
<Hidden smUp implementation="css">
    {content}
</Hidden>

It invokes both contents in both hidden components. it took me a lot of time.

Kjartan
  • 18,591
  • 15
  • 71
  • 96
Mohammad
  • 537
  • 9
  • 6
0

What worked for my project, as it was using React 15.x and React Router v4.x, was to use the render prop rather than component.

Instead of <Route path={${match.url}/exhibit-one} component={ExhibitOne} /> You could have: <Route path={${match.url}/exhibit-one} render={props => <ExhibitOne {...props} />} />

Hope it helps

Aidan
  • 1
  • 2