最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - React Change Image Source on Scroll Event - Stack Overflow

programmeradmin4浏览0评论

As a newer to React but someone familiar with JavaScript, I am having some trouble knowing where exactly to put certain pieces of functionality in a given React ponent, and am not sure if this particular issue I am having is an issue with my React logic or just a lag-time when loading an image, although if it is the latter I do not understand this because I preload the image in a hidden div.

My expected workflow is the following:

  1. Load two images, one "preloaded" with height:0 as part of a hide class.
  2. When the window scrolls, effectively toggle the hide class on either of these two images, so as to effectively change which image is displayed based on the scroll position. The background color of the header will change as well when this happens.

In my ponent Header.jsx I have the following:

ponentDidMount() {
    if (this.props.changeColorOnScroll) {
      window.addEventListener("scroll", this.headerColorChange);
    }
  }
  headerColorChange() {
    console.log("Scroll Fire!");
    const { classes, color, changeColorOnScroll} = this.props;
    const windowsScrollTop = window.pageYOffset;
    if (windowsScrollTop > changeColorOnScroll.height) {
      document.body
        .getElementsByTagName("header")[0]
        .classList.remove(classes[color]);
      document.body
        .getElementsByTagName("header")[0]
        .classList.add(classes[changeColorOnScroll.color]);
     document
        .getElementById("headerLogoContainer").getElementsByClassName("logo")[0]
        .classList.add('hide');
     document
        .getElementById("headerLogoContainer").getElementsByClassName("logoalt")[0]
        .classList.remove('hide');
    } else {
      document.body
        .getElementsByTagName("header")[0]
        .classList.add(classes[color]);
      document.body
        .getElementsByTagName("header")[0]
        .classList.remove(classes[changeColorOnScroll.color]);
     document
        .getElementById("headerLogoContainer").getElementsByClassName("logo")[0]
        .classList.remove('hide');
     document
        .getElementById("headerLogoContainer").getElementsByClassName("logoalt")[0]
        .classList.add('hide');
    }
  } 

I believe this is the right place to attach an event listener in ponentDidMount(). My issue is as I scroll, the window freezes up as the image changes, even though I preload the image here:

render() {
    const {
      classes,
      color,
      rightLinks,
      leftLinks,
      brand,
      logo,
      logoalt,
      fixed,
      absolute
    } = this.props;
    const appBarClasses = classNames({
      [classes.appBar]: true,
      [classes[color]]: color,
      [classes.absolute]: absolute,
      [classes.fixed]: fixed
    });
    let brandComponent;
    if(!logo){
    brandComponent = (
      <Button className={classes.title}>
        {brand}
      </Button>
    );
    }else{
    brandComponent = (
      <div id='headerLogoContainer'>
      <img className='logo'  src={logo}></img>
      <img className='logoalt hide'  src={logoalt}></img>
      </div>
    );
    }
    return (
      <AppBar className={appBarClasses}>
        <Toolbar className={classes.container}>
          {leftLinks !== undefined ? brandComponent : null}
          <div className={classes.flex}>
            {leftLinks !== undefined ? (
              <Hidden smDown implementation="css">
                {leftLinks}
              </Hidden>
            ) : (
              brandComponent
            )}
          </div>

As you can see, in the ponentDidMount() I setup the event listener and then in the render I setup the <div id='headerLogoContainer'> as part of my brandComponent. I see the brandComponent successfully render to the DOM, but on the first scroll (down) it locks up the screen (in Chrome) or doesn't show the logoalt right away (in Edge and Firefox). After a few seconds the images switch and then I can scroll up or down, toggling it with no problem. This leads me to think it might be a preloading issue but why would that be when I have preloaded the image in the brandComponent and see it in the DOM. No additional network requests are being made, just toggling the hide image to show.

So my question is, how can I change an image source in React, based on a scroll event, without a lag or freezing the scroll position?

EDIT:

I've changed my code to leverage internal state according to a suggested answer, and it still has the same effect of a lag when the image changes:

class Header extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      mobileOpen: false,
      logoaltState: false
    };
    this.handleDrawerToggle = this.handleDrawerToggle.bind(this);
    this.headerColorChange = this.headerColorChange.bind(this);
  }
  handleDrawerToggle() {
    this.setState({ mobileOpen: !this.state.mobileOpen });
  }
  ponentDidMount() {
    if (this.props.changeColorOnScroll) {
      window.addEventListener("scroll", this.headerColorChange);
    }
  }
  headerColorChange() {
    console.log("Scroll Fire!");
    const { classes, color, changeColorOnScroll} = this.props;
    const windowsScrollTop = window.pageYOffset;
    if (windowsScrollTop > changeColorOnScroll.height) {
      document.body
        .getElementsByTagName("header")[0]
        .classList.remove(classes[color]);
      document.body
        .getElementsByTagName("header")[0]
        .classList.add(classes[changeColorOnScroll.color]);
        this.setState({ logoaltState: true });
    } else {
      document.body
        .getElementsByTagName("header")[0]
        .classList.add(classes[color]);
      document.body
        .getElementsByTagName("header")[0]
        .classList.remove(classes[changeColorOnScroll.color]);
      this.setState({ logoaltState: false });
    }
  }

and then in my render...

render() {
    const {
      classes,
      color,
      rightLinks,
      leftLinks,
      brand,
      logo,
      logoalt,
      fixed,
      absolute
    } = this.props;
    const{
        mobileOpen,
        logoaltState
    } = this.state
    const appBarClasses = classNames({
      [classes.appBar]: true,
      [classes[color]]: color,
      [classes.absolute]: absolute,
      [classes.fixed]: fixed
    });
    let brandComponent;
    if(!logo){
    brandComponent = (
      <Button className={classes.title}>
        {brand}
      </Button>
    );
    }else{
    brandComponent = (
      <div id='headerLogoContainer'>
      <img className='logo'  src={logo} style={{display: logoaltState === false ? "block" : "none"}}></img>
      <img className='logoalt'   src={logoalt} style={{display: logoaltState === true ? "block" : "none"}}></img>
      </div>
    );
    }

Different code/method, same issue...

EDIT 2:

I don't think it's this application logic that's the issue, because as I mention, it only freezes up the first time the image/state needs to change. I think it's some kind of first-load issue, even though I do not have any network requests, maybe I am including the image source incorrectly? Or in React images need to preloaded by some other method, because the image source is slighty off when it's included... i.e. /static/media/logo.ad924167.png as oppose to require("assets/img/tovlogo.png" I think this is because of my node server settings but again, am not really sure what is going on as I am fairly new to this ecosystem... I'm using the Material UI React Template, that is where this ponent is derived from.

The theme file itself was this: so I am wondering how it handles the images, given the nature of the actual src as opposed to the require... but then again there are no additional requests for the image... it's just a CSS/style change...

EDIT 3:

This is what my debugger (Chrome:Performance) looks like as it is freezing up:

I realize looking at the paint time that the image is quite large, but shouldn't preloading address that... I suppose not because that is a network preload not a paint preload with display:none;"... maybe I can try opacity CSS property instead but this plicates other CSS... is there a better way to do this generally?

FINAL EDIT:

My image is over 10,000 pixels wide... I was using it as a logo so didn't realize until I did the performance metrics how bad it was... I'm guessing this is the reason it froze up... sorry React. :(

As a newer to React but someone familiar with JavaScript, I am having some trouble knowing where exactly to put certain pieces of functionality in a given React ponent, and am not sure if this particular issue I am having is an issue with my React logic or just a lag-time when loading an image, although if it is the latter I do not understand this because I preload the image in a hidden div.

My expected workflow is the following:

  1. Load two images, one "preloaded" with height:0 as part of a hide class.
  2. When the window scrolls, effectively toggle the hide class on either of these two images, so as to effectively change which image is displayed based on the scroll position. The background color of the header will change as well when this happens.

In my ponent Header.jsx I have the following:

ponentDidMount() {
    if (this.props.changeColorOnScroll) {
      window.addEventListener("scroll", this.headerColorChange);
    }
  }
  headerColorChange() {
    console.log("Scroll Fire!");
    const { classes, color, changeColorOnScroll} = this.props;
    const windowsScrollTop = window.pageYOffset;
    if (windowsScrollTop > changeColorOnScroll.height) {
      document.body
        .getElementsByTagName("header")[0]
        .classList.remove(classes[color]);
      document.body
        .getElementsByTagName("header")[0]
        .classList.add(classes[changeColorOnScroll.color]);
     document
        .getElementById("headerLogoContainer").getElementsByClassName("logo")[0]
        .classList.add('hide');
     document
        .getElementById("headerLogoContainer").getElementsByClassName("logoalt")[0]
        .classList.remove('hide');
    } else {
      document.body
        .getElementsByTagName("header")[0]
        .classList.add(classes[color]);
      document.body
        .getElementsByTagName("header")[0]
        .classList.remove(classes[changeColorOnScroll.color]);
     document
        .getElementById("headerLogoContainer").getElementsByClassName("logo")[0]
        .classList.remove('hide');
     document
        .getElementById("headerLogoContainer").getElementsByClassName("logoalt")[0]
        .classList.add('hide');
    }
  } 

I believe this is the right place to attach an event listener in ponentDidMount(). My issue is as I scroll, the window freezes up as the image changes, even though I preload the image here:

render() {
    const {
      classes,
      color,
      rightLinks,
      leftLinks,
      brand,
      logo,
      logoalt,
      fixed,
      absolute
    } = this.props;
    const appBarClasses = classNames({
      [classes.appBar]: true,
      [classes[color]]: color,
      [classes.absolute]: absolute,
      [classes.fixed]: fixed
    });
    let brandComponent;
    if(!logo){
    brandComponent = (
      <Button className={classes.title}>
        {brand}
      </Button>
    );
    }else{
    brandComponent = (
      <div id='headerLogoContainer'>
      <img className='logo'  src={logo}></img>
      <img className='logoalt hide'  src={logoalt}></img>
      </div>
    );
    }
    return (
      <AppBar className={appBarClasses}>
        <Toolbar className={classes.container}>
          {leftLinks !== undefined ? brandComponent : null}
          <div className={classes.flex}>
            {leftLinks !== undefined ? (
              <Hidden smDown implementation="css">
                {leftLinks}
              </Hidden>
            ) : (
              brandComponent
            )}
          </div>

As you can see, in the ponentDidMount() I setup the event listener and then in the render I setup the <div id='headerLogoContainer'> as part of my brandComponent. I see the brandComponent successfully render to the DOM, but on the first scroll (down) it locks up the screen (in Chrome) or doesn't show the logoalt right away (in Edge and Firefox). After a few seconds the images switch and then I can scroll up or down, toggling it with no problem. This leads me to think it might be a preloading issue but why would that be when I have preloaded the image in the brandComponent and see it in the DOM. No additional network requests are being made, just toggling the hide image to show.

So my question is, how can I change an image source in React, based on a scroll event, without a lag or freezing the scroll position?

EDIT:

I've changed my code to leverage internal state according to a suggested answer, and it still has the same effect of a lag when the image changes:

class Header extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      mobileOpen: false,
      logoaltState: false
    };
    this.handleDrawerToggle = this.handleDrawerToggle.bind(this);
    this.headerColorChange = this.headerColorChange.bind(this);
  }
  handleDrawerToggle() {
    this.setState({ mobileOpen: !this.state.mobileOpen });
  }
  ponentDidMount() {
    if (this.props.changeColorOnScroll) {
      window.addEventListener("scroll", this.headerColorChange);
    }
  }
  headerColorChange() {
    console.log("Scroll Fire!");
    const { classes, color, changeColorOnScroll} = this.props;
    const windowsScrollTop = window.pageYOffset;
    if (windowsScrollTop > changeColorOnScroll.height) {
      document.body
        .getElementsByTagName("header")[0]
        .classList.remove(classes[color]);
      document.body
        .getElementsByTagName("header")[0]
        .classList.add(classes[changeColorOnScroll.color]);
        this.setState({ logoaltState: true });
    } else {
      document.body
        .getElementsByTagName("header")[0]
        .classList.add(classes[color]);
      document.body
        .getElementsByTagName("header")[0]
        .classList.remove(classes[changeColorOnScroll.color]);
      this.setState({ logoaltState: false });
    }
  }

and then in my render...

render() {
    const {
      classes,
      color,
      rightLinks,
      leftLinks,
      brand,
      logo,
      logoalt,
      fixed,
      absolute
    } = this.props;
    const{
        mobileOpen,
        logoaltState
    } = this.state
    const appBarClasses = classNames({
      [classes.appBar]: true,
      [classes[color]]: color,
      [classes.absolute]: absolute,
      [classes.fixed]: fixed
    });
    let brandComponent;
    if(!logo){
    brandComponent = (
      <Button className={classes.title}>
        {brand}
      </Button>
    );
    }else{
    brandComponent = (
      <div id='headerLogoContainer'>
      <img className='logo'  src={logo} style={{display: logoaltState === false ? "block" : "none"}}></img>
      <img className='logoalt'   src={logoalt} style={{display: logoaltState === true ? "block" : "none"}}></img>
      </div>
    );
    }

Different code/method, same issue...

EDIT 2:

I don't think it's this application logic that's the issue, because as I mention, it only freezes up the first time the image/state needs to change. I think it's some kind of first-load issue, even though I do not have any network requests, maybe I am including the image source incorrectly? Or in React images need to preloaded by some other method, because the image source is slighty off when it's included... i.e. /static/media/logo.ad924167.png as oppose to require("assets/img/tovlogo.png" I think this is because of my node server settings but again, am not really sure what is going on as I am fairly new to this ecosystem... I'm using the Material UI React Template, that is where this ponent is derived from.

The theme file itself was this: https://www.creative-tim./product/material-kit-react?partner=104080 so I am wondering how it handles the images, given the nature of the actual src as opposed to the require... but then again there are no additional requests for the image... it's just a CSS/style change...

EDIT 3:

This is what my debugger (Chrome:Performance) looks like as it is freezing up:

I realize looking at the paint time that the image is quite large, but shouldn't preloading address that... I suppose not because that is a network preload not a paint preload with display:none;"... maybe I can try opacity CSS property instead but this plicates other CSS... is there a better way to do this generally?

FINAL EDIT:

My image is over 10,000 pixels wide... I was using it as a logo so didn't realize until I did the performance metrics how bad it was... I'm guessing this is the reason it froze up... sorry React. :(

Share edited Jul 20, 2018 at 15:40 Summer Developer asked Jul 8, 2018 at 22:44 Summer DeveloperSummer Developer 2,0968 gold badges35 silver badges68 bronze badges
Add a ment  | 

1 Answer 1

Reset to default 6

Instead of manually changing classes on DOM elements, I think it will be easier for you if you instead leverage the ponent state and switch between the active image depending on the scroll position.

Example

class App extends React.Component {
  state = { active: 0 };

  ponentDidMount() {
    window.addEventListener("scroll", this.handleScroll);
  }

  ponentWillUnmount() {
    window.removeEventListener("scroll", this.handleScroll);
  }

  handleScroll = event => {
    const { pageYOffset } = window;
    const { active } = this.state;

    if (pageYOffset >= 500 && active === 0) {
      this.setState({ active: 1 });
    } else if (pageYOffset < 500 && active === 1) {
      this.setState({ active: 0 });
    }
  };

  render() {
    const { active } = this.state;
    return (
      <div style={{ height: 1000, width: 1000 }}>
        <div
          style={{
            height: "100%",
            width: "100%",
            backgroundColor: "red",
            display: active === 0 ? "block" : "none"
          }}
        />
        <div
          style={{
            height: "100%",
            width: "100%",
            backgroundColor: "blue",
            display: active === 1 ? "block" : "none"
          }}
        />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare./ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

发布评论

评论列表(0)

  1. 暂无评论