Since updating react-dom and react-router-dom packages: excessive li onClick behavior

白昼怎懂夜的黑 提交于 2020-03-25 19:20:34

问题


I've inherited a react/node/prismic application where we need to update the prismic-reactjs package, which resulted in needing to update a few others- in total, we changed:

prismic-reactjs: 0.2.0 → 1.1.0

react: 15.6.1 → 16.0.0

webpack: 3.12.0 → 4.39.2

react-dom: 15.6.1 → 16.0.0

react-router-dom: 4.1.2 → 5.0.1

extract-text-webpack-plugin (deprecated) → mini-css-extract-plugin

Then removed one use of "withRouter()" because of new error when starting up the local server (but I confirmed in another branch that making that edit alone doesn't produce the symptom below)

Context: we have a personMap that shows a icons for a set of people, and you click one of them to open a PersonMapStory modal, which shows that person's story, then links to everybody else in a list at the bottom. If you scroll down and click one of those links, the story up top is replaced accordingly (with the new person's story), and list below is everyone else. In the code, the button onClick behavior for these bottom links is setActivePerson() to this new person.

New bug: in this new branch, when we click a bottom link it calls setActivePerson() for person #2, but then also calls setActivePerson() for person #1 again! (Back from the original PersonMap li onClick behavior). So the symptom is that you see the first story, scroll down and click a different person's link, but the modal doesn't appear to update at all (though it's really updating twice). In the current/production/working branch it just updates once to the new person and that's that. This new behavior sounds reeeally similar to this onClick propagation issue. I tried adding stopPropagation() into the appropriate methods, but no luck.

Further info: in this branch, it looks like ANY click from inside that modal (even if I click on the story that's displayed up top) calls SetActivePerson() with the original person, from the PersonMap line item onClick behavior, when thisPersonIsQueued is true. (So if I scroll down and click a new person, we SetActivePerson person #2, followed immediately by SetActivePerson person #1, so the modal keeps the same story and never appears to update). In the PersonMap state (viewed in Chrome Developer Tools/Components) there's a queuedUpPatient: typically in this scenario (in the original/production code) queuedUpPatient is null, but in this new branch, queuedUpPatient keeps the value of Person #1. (or has it cleared out and re-set).

Possible clue in the debugger: After we call SetActivePerson for #2 (which is the last step that happens in our regular/production branch), before we see the setActivePerson call for person #1 from it's onClick behavior, the debugger takes me through several methods in react-dom.development.js, starting with the very last line of callCallback(), inside code for the following occasion (see below):

// Check that the browser supports the APIs we need to implement our special
// DEV version of invokeGuardedCallback
if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document 

Other ideas for what could be going on and how I can get this working normally again so we can go ahead with the package upgrades? Here are the two relevant files for the map and the modal (below).

PersonMap.js:

import Modal from 'react-modal'
import PropTypes from 'prop-types'
import React from 'react'

import PersonMapPoint from './PersonMapPoint'
import PersonMapStory from './PersonMapStory'
import PersonMapCallout from './PersonMapCallout'
import PersonMapLocator from './PersonMapLocator'
import PersonMapBackground from './PersonMapBackground'

const CUSTOM_STYLES = {
    content: {
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        backgroundColor: '#fff',
        zIndex: 10,
        border: 'none'
    }
}

class PersonMap extends React.Component {
    constructor(props) {
        super(props)

        this.setActivePerson = this.setActivePerson.bind(this)
        this.setNoActivePerson = this.setNoActivePerson.bind(this)
        this.setQueuedUpPerson = this.setQueuedUpPerson.bind(this)
        this.setNoQueuedUpPerson = this.setNoQueuedUpPerson.bind(this)
        this.checkBlurEvent = this.checkBlurEvent.bind(this)
        this.setIsDesktop = this.setIsDesktop.bind(this)
        this.checkHasBeenScrolledTo = this.checkHasBeenScrolledTo.bind(this)
        this.setTopRef = this.setTopRef.bind(this)
        this.handleKeyPress = this.handleKeyPress.bind(this)

        this.state = {
            activePerson: null,
            queuedUpPerson: null,
            scrollPos: 0,
            isDesktop: false,
            hasBeenScrolledTo: false,
            lastQueuedPerson: null
        }
    }

    componentDidMount() {
        this.setIsDesktop()
        this.checkHasBeenScrolledTo()
        window.addEventListener('resize', this.setIsDesktop)
        window.addEventListener('scroll', this.checkHasBeenScrolledTo)
    }

    componentWillUnmount() {
        window.removeEventListener('resize', this.setIsDesktop)
        window.removeEventListener('scroll', this.checkHasBeenScrolledTo)
    }

    setTopRef(element) {
        this.topRef = element
    }

    setActivePerson(personName) {
        this.setState({
            activePerson: personName,
            scrollPos: window.scrollY
        })
        event.stopPropagation()
    }

    setNoActivePerson() {
        this.setState({
            queuedUpPerson: this.state.activePerson,
            activePerson: null
        }, () => {
            setTimeout(() => {
                window.scrollTo(0, this.state.scrollPos)
            }, 50)
        })
    }

    setQueuedUpPerson(personName) {
        this.setState({
            queuedUpPerson: personName,
            lastQueuedPerson: personName
        })
    }

    setNoQueuedUpPerson() {
        this.setState({
            queuedUpPerson: null
        })
        event.stopPropagation()
    }

    handleKeyPress(e, name) {
        if (e.key !== ' ' && e.key !== 'Enter') {
            return
        }
        this.setActivePerson(name)
    }

    checkBlurEvent(e) {
        if (Array.from(e.currentTarget.childNodes[0].childNodes).includes(e.relatedTarget)) {
            return
        }
        this.setNoQueuedUpPerson()
    }

    render() {
        return (
            <section className="slice-area person-map CONSTRAIN">
                <div className="person-map-headers">
                    <div className="person-map-headers-inner">
                        <h1 className="person-map-title">
                            {this.props.title}
                        </h1>
                        <p className="person-map-disclaimer">
                            {this.props.disclaimer}
                        </p>
                    </div>
                </div>
                <div
                    className={`person-map-list-wrap${ this.state.isDesktop ? ' --desktop' : '' }`}
                    ref={this.state.isDesktop && this.setTopRef}
                >
                    { this.state.isDesktop &&
                        <PersonMapBackground isVisible={this.state.hasBeenScrolledTo}/>
                    }
                    <ul className="person-map-list">
                        {
                            this.props.personStories.map((person) => {
                                const thisPersonIsQueued = this.state.queuedUpPerson === person.name
                                const thisPersonIsActive = this.state.activePerson === person.name
                                const thisPersonWasLastQueued = this.state.lastQueuedPerson === person.name

                                return (
                                    <li
                                        key={person.name} className={`person-map-list-item${thisPersonWasLastQueued ? ' --active' : ''}`}
                                        onMouseEnter={this.state.isDesktop ? () => this.setQueuedUpPerson(person.name) : null}
                                        onMouseLeave={this.state.isDesktop ? this.setNoQueuedUpPerson : null}
                                        onFocus={this.state.isDesktop ? () => this.setQueuedUpPerson(person.name) : null}
                                        onBlur={this.state.isDesktop ? this.checkBlurEvent : null}
                                        onClick={thisPersonIsQueued || !this.state.isDesktop ? () => this.setActivePerson(person.name) : null}
                                        onKeyPress={(e) => this.handleKeyPress(e, person.name)}
                                    >
                                        {
                                                <PersonMapLocator
                                                    x={person.x}
                                                    y={person.y}
                                                >
                                        }
                                        <Modal
                                            isOpen={thisPersonIsActive}
                                            onRequestClose={this.setNoActivePerson}
                                            style={CUSTOM_STYLES}
                                        >
                                            <PersonMapStory
                                                name={person.name}
                                                photo={person.photo_url}
                                                story={person.story}
                                                setNoActivePerson={this.setNoActivePerson}
                                                setActivePerson={this.setActivePerson}
                                                isActive={thisPersonIsActive}
                                                otherPersons={this.props.personStories.filter((item) => item.name !== person.name).map((item) => ({ name: item.name, photo: item.photo_url }))}
                                                isDesktop={this.state.isDesktop}
                                            />
                                        </Modal>
                                    </li>
                                )
                            })
                        }
                    </ul>
                </div>
            </section>
        )
    }
}

PersonMap.propTypes = {
    title: PropTypes.string,
    disclaimer: PropTypes.string,
    personStories: PropTypes.arrayOf(PropTypes.shape({
        name: PropTypes.string,
        photo_url: PropTypes.string,
        x: PropTypes.number,
        y: PropTypes.number,
        story: PropTypes.shape({
            title: PropTypes.string,
            link: PropTypes.string,
            content: PropTypes.arrayOf(PropTypes.shape({
                question: PropTypes.string,
                answer: PropTypes.string
            }))
        })
    }))
}
export default PersonMap

PersonMapStory.js:

import PropTypes from 'prop-types'
import React from 'react'

import PersonMapPerson from './PersonMapPerson'


class PersonMapStory extends React.Component {
    constructor(props) {
        console.log("PersonMapStory constructor")
        super(props)

        this.deactivateThisPerson = this.deactivateThisPerson.bind(this)
        this.check = 0
    }

    deactivateThisPerson(backPressed = false) {
        if (!backPressed) {
            history.back()
        }
        console.log("Set no active person ")
        this.props.setNoActivePerson()
    }

    setActivePerson(name) {
        this.props.setActivePerson(name)
        event.stopPropagation()
    }


    componentDidMount() {
        const loc = window.location.pathname.substr(1)
        this.setState({ location: loc })
        const openState = { modalOpen: true }
        history.pushState(openState, 'signup-open', loc)
      }

    render() {
        return (
            <div className={`person-map-story${this.props.isActive ? ' --active' : ''}${this.props.isDesktop ? ' --desktop' : ''}`}>
                <h2 className="person-map-story-title">
                    { this.props.story.title }
                </h2>
                <button className="person-map-story-close-button" onClick={() => {this.deactivateThisPerson(false)}}>
                    <span className="person-map-story-close-button-text">
                        Close Story
                    </span>
                </button>
                <div className="person-map-story-body">
                    <span className="person-map-story-person-wrap">
                        <PersonMapPerson
                            photo={this.props.photo}
                            name={this.props.name}
                        />
                    </span>
                    {
                        this.props.story.content.map((section) => {
                            if (section) {
                                return (
                                    <div key={section.question}>
                                        <h3 className="person-map-story-question">{section.question}</h3>
                                        <p className="person-map-story-answer">
                                            {section.answer}
                                        </p>
                                    </div>
                                )
                            }
                        })
                    }
                    {
                        this.props.story.link &&
                        <a  href={this.props.story.link}
                            target="_blank"
                            className="person-map-story-more-link header-and-text-link"
                        >  Read More Stories
                        </a>
                    }

                    <ul className="person-map-story-other-list">
                        {
                            this.props.otherPersons.map((person) => (
                                <li
                                    key={person.name}
                                    className="person-map-story-other-list-item"
                                >
                                    <button className="person-map-story-other-button" onClick={() => this.setActivePerson(person.name)}>
                                        <PersonMapPerson
                                            name={person.name}
                                            photo={person.photo}
                                            ctaText="View their story"
                                        />
                                    </button>
                                </li>
                            ))
                        }
                    </ul>
                </div>
            </div>
        )
    }
}

PersonMapStory.propTypes = {
    name: PropTypes.string,
    photo: PropTypes.string,
    story: PropTypes.shape({
        title: PropTypes.string,
        link: PropTypes.string,
        content: PropTypes.arrayOf(PropTypes.shape({
            question: PropTypes.string,
            answer: PropTypes.string
        }))
    }),
    setNoActivePerson: PropTypes.func,
    setActivePerson: PropTypes.func,
    isActive: PropTypes.bool,
    otherPersons: PropTypes.arrayOf(PropTypes.shape({
        name: PropTypes.string,
        photo: PropTypes.string
    })),
    isDesktop: PropTypes.bool
}

export default PersonMapStory

回答1:


Update your onClick in PersonMapStory to access the event object:

<button className="person-map-story-other-button" onClick={e => this.setActivePerson(e, person.name)}>

and change the setActivePerson(name) function in PersonMapStory:

setActivePerson(event, name) {
    this.props.setActivePerson(name)
    event.stopPropagation()
}


来源:https://stackoverflow.com/questions/58530473/since-updating-react-dom-and-react-router-dom-packages-excessive-li-onclick-beh

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!