简体   繁体   中英

Mutating child components based on parent component's state, without re-rendering

Goal

I'm trying to manage mouseenter and mouseleave events from a parent component, for a collection of child components that keep getting re-rendered.

I'm building a reusable component for the collection of listings, that does several things like pagination, and a few other things when a listing is hovered.

So to make this reusable, I have to maintain the state of the hovered listing from the parent CollectionComponent , and mutate each individual listing component based on the state of the parent.


Code

Here are the components I'm using (I stripped them all down to their most basic forms):

Listings Component:

import React from 'react'
import $ from 'jquery'
import CollectionComponent from './CollectionComponent'
import Listing from './Listing'

export default class Listings extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      listings: this.props.listings,
    }
  }

  render() {

    return (<section className="listing-results">
      {this.state.listings.map( listing => 
        <CollectionComponent results={this.state.listings} IndividualResult={Listing} perPage={this.props.perPage} options={options}/> 
      )}
    </section>)
  }
}

Collection Component:

import React from 'react'

export default class CollectionComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      results: this.props.results,
      hoveredId: null
    }
  }

  componentDidMount() {
    this.$listings = $('.result-card')
    $(this.$listings).mouseenter(this.toggleInfoIn).mouseleave(this.toggleInfoOut)
  }

  toggleInfoIn = e => {
    var { target } = e
    var infoId = $(target).data('id')
    this.setState({hoveredId: infoId})
  }

  toggleInfoOut = e => {
    this.setState({hoveredId: null})
  }

  render() {
    const {results, IndividualResult, perPage, options} = this.props

    return (
      <div className="paginated-results">
        {this.state.results.map( result =>  
          <IndividualResult key={result.id} result={result} options={options}/> 
        )}
      </div>
    )
  }
}

Individual Listing Component:

import React from 'react'

export default class Listing extends React.Component {
  constructor(props) {
    super(props)
  }

  render() {

    const { listing, hoveredId } = this.props

    return (
      <div className="result-card" data-id={listing.id}>
        <div className={hoveredId === listing.id ? 'hovered' : ''}>
          Listing Content
        </div>
      </div>
    )
  }
}

I know I can probably structure the CollectionComponent a little cleaner with a higher order component, but I'll leave that for refactoring later once I get it working properly with this basic setup.


Problem

My problem is that every time I hover and change the state of the parent component, it re-renders the child components, because their props are dependent on the parent's state. Once this happens, the reference to my jQuery collection of listings is no longer valid. So the mouse events are attached to old DOM elements that no longer exist.


How can I structure this differently, so that either:

  1. the child elements' props update without re-rendering, or
  2. the jQuery collection reference doesn't change

I'd really like to avoid getting a new the jQuery collection every time the component updates.

The behavior of hover should be confined to the individual listing component and not the Collections component.

As the Collections component maintains the state of currently hovered item, it is good idea to pass an handler as part of props and then render the list again based on the change in state set by the Collections component.

Use react based event handlers where ever necessary which makes it for a controlled component. It is not a good idea to put state in the DOM where react can take care of it for you.

Listings

import React from 'react'

export default class Listing extends React.Component {
  constructor(props) {
    super(props);

    this.onMouseEnter = this.onMouseEnter.bind(this);
    this.onMouseLeave = this.onMouseLeave.bind(this);
  }
  onMouseEnter() {
        this.props.onMouseEnter({ listingId: this.props.listing.id });
  }
  onMouseLeave() {
        this.props.onMouseLeave();
  }
  render() {

    const { listing, hoveredId } = this.props
    const listingId = listing.id;
    const isHovered = this.props.hoveredId === listing.id;

    return (
      <div className="result-card" onMouseEnter={this.onMouseEnter} onMouseLeave={onMouseLeave}>
        <div className={isHovered ? 'hovered' : ''}>
          Listing Content
        </div>
      </div>
    )
  }
}

Collections

import React from 'react'

export default class CollectionComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      results: this.props.results,
      hoveredId: null
    }
  }

  onMouseEnter({ listingId }) {
       this.setState({ listingId });
  }
  onMouseLeave() {
       this.setState({ listingId: null });
  }

  render() {
    const {results, IndividualResult, perPage, options} = this.props

    return (
      <div className="paginated-results">
        {this.state.results.map( result =>  
          <IndividualResult key={result.id} hoveredId={this.state.hoveredId} result={result} options={options} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}/> 
        )}
      </div>
    )
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM