Aktualisieren Sie den Stil einer Komponente onScroll in React.js

132

Ich habe in React eine Komponente erstellt, die ihren eigenen Stil beim Scrollen im Fenster aktualisieren soll, um einen Parallaxeeffekt zu erzeugen.

Die Komponentenmethode rendersieht folgendermaßen aus:

  function() {
    let style = { transform: 'translateY(0px)' };

    window.addEventListener('scroll', (event) => {
      let scrollTop = event.srcElement.body.scrollTop,
          itemTranslate = Math.min(0, scrollTop/3 - 60);

      style.transform = 'translateY(' + itemTranslate + 'px)');
    });

    return (
      <div style={style}></div>
    );
  }

Dies funktioniert nicht, da React nicht weiß, dass sich die Komponente geändert hat, und daher die Komponente nicht erneut gerendert wird.

Ich habe versucht, den Wert von itemTranslateim Status der Komponente zu speichern und setStateden Scroll-Rückruf aufzurufen . Dies macht das Scrollen jedoch unbrauchbar, da dies sehr langsam ist.

Irgendwelche Vorschläge, wie das geht?

Alejandro Pérez
quelle
9
Binden Sie niemals einen externen Ereignishandler in eine Rendermethode. Rendering-Methoden (und alle anderen benutzerdefinierten Methoden, die Sie renderim selben Thread aufrufen ) sollten sich nur mit der Logik befassen, die sich auf das Rendern / Aktualisieren des tatsächlichen DOM in React bezieht. Stattdessen sollten Sie, wie von @AustinGreco unten gezeigt, die angegebenen React-Lebenszyklusmethoden verwenden, um Ihre Ereignisbindung zu erstellen und zu entfernen. Dies macht es in der Komponente in sich geschlossen und stellt sicher, dass kein Leck auftritt, indem sichergestellt wird, dass die Ereignisbindung entfernt wird, wenn die Komponente, die sie verwendet, nicht gemountet ist.
Mike Driver

Antworten:

232

Sie sollten den Listener einbinden componentDidMount, auf diese Weise wird er nur einmal erstellt. Sie sollten in der Lage sein, den Stil im Status zu speichern. Der Listener war wahrscheinlich die Ursache für Leistungsprobleme.

Etwas wie das:

componentDidMount: function() {
    window.addEventListener('scroll', this.handleScroll);
},

componentWillUnmount: function() {
    window.removeEventListener('scroll', this.handleScroll);
},

handleScroll: function(event) {
    let scrollTop = event.srcElement.body.scrollTop,
        itemTranslate = Math.min(0, scrollTop/3 - 60);

    this.setState({
      transform: itemTranslate
    });
},
Austin Greco
quelle
26
Ich fand, dass setState'ing innerhalb des Scroll-Ereignisses für die Animation abgehackt ist. Ich musste den Stil der Komponenten mithilfe von Refs manuell einstellen.
Ryan Rho
1
Worauf würde das "dies" in handleScroll hinweisen? In meinem Fall ist es "Fenster" keine Komponente.
Yuji
10
@yuji Sie können vermeiden, dass die Komponente übergeben werden muss, indem Sie dies im Konstruktor this.handleScroll = this.handleScroll.bind(this)binden : Bindet dies innerhalb handleScrollder Komponente anstelle des Fensters.
Matt Parrilla
1
Beachten Sie, dass srcElement in Firefox nicht verfügbar ist.
Paulin Trognon
2
hat bei mir nicht funktioniert, aber scrollTop wurde aufevent.target.scrollingElement.scrollTop
George
31

Sie können dem onScrollEreignis im Element React eine Funktion übergeben : https://facebook.github.io/react/docs/events.html#ui-events

<ScrollableComponent
 onScroll={this.handleScroll}
/>

Eine andere Antwort, die ähnlich ist: https://stackoverflow.com/a/36207913/1255973

Con Antonakos
quelle
5
Gibt es Vor- oder Nachteile dieser Methode gegenüber dem manuellen Hinzufügen eines Ereignis-Listeners zum genannten Fensterelement @AustinGreco?
Dennis
2
@Dennis Ein Vorteil ist, dass Sie die Ereignis-Listener nicht manuell hinzufügen / entfernen müssen. Dies ist zwar ein einfaches Beispiel, wenn Sie mehrere Ereignis-Listener in Ihrer gesamten Anwendung manuell verwalten. Es ist jedoch leicht zu vergessen, diese bei Aktualisierungen ordnungsgemäß zu entfernen, was zu Speicherfehlern führen kann. Ich würde immer die eingebaute Version verwenden, wenn möglich.
F Lekschas
4
Es ist erwähnenswert, dass dadurch ein Bildlauf-Handler an die Komponente selbst angehängt wird, nicht an das Fenster, was eine ganz andere Sache ist. @Dennis Die Vorteile von onScroll sind, dass es browserübergreifender und leistungsfähiger ist. Wenn Sie es verwenden können, sollten Sie es wahrscheinlich tun, aber es kann in Fällen wie dem für das OP
Beau
20

Meine Lösung für die Erstellung einer reaktionsschnellen Navigationsleiste (Position: 'relativ', wenn nicht gescrollt wird, und fest beim Scrollen und nicht oben auf der Seite)

componentDidMount() {
    window.addEventListener('scroll', this.handleScroll);
}

componentWillUnmount() {
    window.removeEventListener('scroll', this.handleScroll);
}
handleScroll(event) {
    if (window.scrollY === 0 && this.state.scrolling === true) {
        this.setState({scrolling: false});
    }
    else if (window.scrollY !== 0 && this.state.scrolling !== true) {
        this.setState({scrolling: true});
    }
}
    <Navbar
            style={{color: '#06DCD6', borderWidth: 0, position: this.state.scrolling ? 'fixed' : 'relative', top: 0, width: '100vw', zIndex: 1}}
        >

Keine Leistungsprobleme für mich.

robins_
quelle
Sie können auch einen gefälschten Header verwenden, der im Wesentlichen nur ein Platzhalter ist. Sie haben also Ihren festen Header und darunter Ihren Platzhalter-Fakeheader mit der Position: relative.
robins_
Keine Leistungsprobleme, da hierdurch die Parallaxenherausforderung in der Frage nicht behoben wird.
Juanitogan
19

Um allen hier zu helfen, die die verzögerten Verhaltens- / Leistungsprobleme bei der Verwendung der Austins-Antwort bemerkt haben und ein Beispiel mit den in den Kommentaren erwähnten Refs wünschen, ist hier ein Beispiel, das ich zum Umschalten einer Klasse für ein Bildlauf-Auf / Ab-Symbol verwendet habe:

In der Rendermethode:

<i ref={(ref) => this.scrollIcon = ref} className="fa fa-2x fa-chevron-down"></i>

In der Handler-Methode:

if (this.scrollIcon !== null) {
  if(($(document).scrollTop() + $(window).height() / 2) > ($('body').height() / 2)){
    $(this.scrollIcon).attr('class', 'fa fa-2x fa-chevron-up');
  }else{
    $(this.scrollIcon).attr('class', 'fa fa-2x fa-chevron-down');
  }
}

Und fügen Sie Ihre Handler auf die gleiche Weise wie in Austin hinzu / entfernen Sie sie:

componentDidMount(){
  window.addEventListener('scroll', this.handleScroll);
},
componentWillUnmount(){
  window.removeEventListener('scroll', this.handleScroll);
},

Dokumente auf den Refs .

adrian_reimer
quelle
4
Du hast meinen Tag gerettet! Zum Aktualisieren müssen Sie an dieser Stelle keine jquery verwenden, um den Klassennamen zu ändern, da es sich bereits um ein natives DOM-Element handelt. Sie könnten es also einfach tun this.scrollIcon.className = whatever-you-want.
Southp
2
Diese Lösung bricht React-Kapselung ab, obwohl ich mir immer noch nicht sicher bin, wie ich dies ohne verzögertes Verhalten umgehen kann - vielleicht wäre ein entprelltes Scroll-Ereignis (bei vielleicht 200-250 ms) hier eine Lösung
Jordanien
Nope Debounce Scroll-Ereignis hilft nur dabei, das Scrollen flüssiger zu machen (im nicht blockierenden Sinne), aber es dauert 500 ms bis eine Sekunde, bis die Aktualisierungen im DOM angewendet werden: /
Jordan
Ich habe auch diese Lösung verwendet, +1. Ich bin damit einverstanden, dass Sie jQuery nicht benötigen: Verwenden Sie einfach classNameoder classList. Außerdem brauchte ich nichtwindow.addEventListener() : Ich habe nur React's verwendet onScrollund es ist so schnell, solange Sie Requisiten / Status nicht aktualisieren!
Benjamin
13

Ich habe festgestellt, dass ich den Ereignis-Listener nur dann erfolgreich hinzufügen kann, wenn ich true wie folgt übergebe:

componentDidMount = () => {
    window.addEventListener('scroll', this.handleScroll, true);
},
Jean-Marie Dalmasso
quelle
Es funktioniert. Aber können Sie sich vorstellen, warum wir diesem Hörer einen echten Booleschen Wert übergeben müssen?
Shah Chaitanya
2
Von w3schools: [ w3schools.com/jsref/met_document_addeventlistener.asp] userCapture : Optional. Ein boolescher Wert, der angibt, ob das Ereignis in der Erfassungs- oder in der Blasenphase ausgeführt werden soll. Mögliche Werte: true - Der Ereignishandler wird in der Erfassungsphase false- Default ausgeführt. Der Event-Handler wird in der Blasenphase ausgeführt
Jean-Marie Dalmasso
12

Ein Beispiel mit classNames , React hooks useEffect , useState und styled-jsx :

import classNames from 'classnames'
import { useEffect, useState } from 'react'

const Header = _ => {
  const [ scrolled, setScrolled ] = useState()
  const classes = classNames('header', {
    scrolled: scrolled,
  })
  useEffect(_ => {
    const handleScroll = _ => { 
      if (window.pageYOffset > 1) {
        setScrolled(true)
      } else {
        setScrolled(false)
      }
    }
    window.addEventListener('scroll', handleScroll)
    return _ => {
      window.removeEventListener('scroll', handleScroll)
    }
  }, [])
  return (
    <header className={classes}>
      <h1>Your website</h1>
      <style jsx>{`
        .header {
          transition: background-color .2s;
        }
        .header.scrolled {
          background-color: rgba(0, 0, 0, .1);
        }
      `}</style>
    </header>
  )
}
export default Header
Giovannipds
quelle
1
Beachten Sie, dass dieses useEffect kein zweites Argument enthält und jedes Mal ausgeführt wird, wenn der Header gerendert wird.
Jordanien
2
@ Jordan du hast recht! Mein Fehler, das hier zu schreiben. Ich werde die Antwort bearbeiten. Vielen Dank.
Giovannipds
8

mit Haken

import React, { useEffect, useState } from 'react';

function MyApp () {

  const [offset, setOffset] = useState(0);

  useEffect(() => {
    window.onscroll = () => {
      setOffset(window.pageYOffset)
    }
  }, []);

  console.log(offset); 
};
Sodium United
quelle
Genau das, was ich brauchte. Vielen Dank!
Aabbccsmith
Dies ist bei weitem die effektivste und eleganteste Antwort von allen. Danke dafür.
Chigozie Orunta
Das braucht mehr Aufmerksamkeit, perfekt.
Anders Kitson
6

Beispiel für eine Funktionskomponente mit useEffect:

Hinweis : Sie müssen den Ereignis-Listener entfernen, indem Sie in useEffect eine "Bereinigungs" -Funktion zurückgeben. Wenn Sie dies nicht tun, erhalten Sie jedes Mal, wenn die Komponente aktualisiert wird, einen zusätzlichen Fenster-Scroll-Listener.

import React, { useState, useEffect } from "react"

const ScrollingElement = () => {
  const [scrollY, setScrollY] = useState(0);

  function logit() {
    setScrollY(window.pageYOffset);
  }

  useEffect(() => {
    function watchScroll() {
      window.addEventListener("scroll", logit);
    }
    watchScroll();
    // Remove listener (like componentWillUnmount)
    return () => {
      window.removeEventListener("scroll", logit);
    };
  }, []);

  return (
    <div className="App">
      <div className="fixed-center">Scroll position: {scrollY}px</div>
    </div>
  );
}
Richard
quelle
Beachten Sie, dass dieses useEffect kein zweites Argument enthält und jedes Mal ausgeführt wird, wenn die Komponente gerendert wird.
Jordanien
Guter Punkt. Sollten wir dem useEffect-Aufruf ein leeres Array hinzufügen?
Richard
Das würde ich tun :)
Jordanien
5

Wenn Sie an einer untergeordneten Komponente interessiert sind, die einen Bildlauf durchführt, kann dieses Beispiel hilfreich sein: https://codepen.io/JohnReynolds57/pen/NLNOyO?editors=0011

class ScrollAwareDiv extends React.Component {
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
    this.state = {scrollTop: 0}
  }

  onScroll = () => {
     const scrollTop = this.myRef.current.scrollTop
     console.log(`myRef.scrollTop: ${scrollTop}`)
     this.setState({
        scrollTop: scrollTop
     })
  }

  render() {
    const {
      scrollTop
    } = this.state
    return (
      <div
         ref={this.myRef}
         onScroll={this.onScroll}
         style={{
           border: '1px solid black',
           width: '600px',
           height: '100px',
           overflow: 'scroll',
         }} >
        <p>This demonstrates how to get the scrollTop position within a scrollable 
           react component.</p>
        <p>ScrollTop is {scrollTop}</p>
     </div>
    )
  }
}
user2312410
quelle
1

Ich habe das Problem durch die Verwendung und Änderung von CSS-Variablen gelöst. Auf diese Weise muss ich den Komponentenstatus nicht ändern, was zu Leistungsproblemen führt.

index.css

:root {
  --navbar-background-color: rgba(95,108,255,1);
}

Navbar.jsx

import React, { Component } from 'react';
import styles from './Navbar.module.css';

class Navbar extends Component {

    documentStyle = document.documentElement.style;
    initalNavbarBackgroundColor = 'rgba(95, 108, 255, 1)';
    scrolledNavbarBackgroundColor = 'rgba(95, 108, 255, .7)';

    handleScroll = () => {
        if (window.scrollY === 0) {
            this.documentStyle.setProperty('--navbar-background-color', this.initalNavbarBackgroundColor);
        } else {
            this.documentStyle.setProperty('--navbar-background-color', this.scrolledNavbarBackgroundColor);
        }
    }

    componentDidMount() {
        window.addEventListener('scroll', this.handleScroll);
    }

    componentWillUnmount() {
        window.removeEventListener('scroll', this.handleScroll);
    }

    render () {
        return (
            <nav className={styles.Navbar}>
                <a href="/">Home</a>
                <a href="#about">About</a>
            </nav>
        );
    }
};

export default Navbar;

Navbar.module.css

.Navbar {
    background: var(--navbar-background-color);
}
Webpreneur
quelle
1

Meine Wette hier ist die Verwendung von Funktionskomponenten mit neuen Hooks, um das Problem zu lösen, aber anstatt useEffectwie in den vorherigen Antworten zu verwenden, denke ich, dass die richtige Option useLayoutEffectaus einem wichtigen Grund wäre:

Die Signatur ist identisch mit useEffect, wird jedoch nach allen DOM-Mutationen synchron ausgelöst.

Dies finden Sie in der React-Dokumentation . Wenn wir useEffectstattdessen verwenden und die bereits gescrollte Seite neu laden, ist das Scrollen falsch und unsere Klasse wird nicht angewendet, was zu einem unerwünschten Verhalten führt.

Ein Beispiel:

import React, { useState, useLayoutEffect } from "react"

const Mycomponent = (props) => {
  const [scrolled, setScrolled] = useState(false)

  useLayoutEffect(() => {
    const handleScroll = e => {
      setScrolled(window.scrollY > 0)
    }

    window.addEventListener("scroll", handleScroll)

    return () => {
      window.removeEventListener("scroll", handleScroll)
    }
  }, [])

  ...

  return (
    <div className={scrolled ? "myComponent--scrolled" : ""}>
       ...
    </div>
  )
}

Eine mögliche Lösung für das Problem könnte https://codepen.io/dcalderon/pen/mdJzOYq sein

const Item = (props) => { 
  const [scrollY, setScrollY] = React.useState(0)

  React.useLayoutEffect(() => {
    const handleScroll = e => {
      setScrollY(window.scrollY)
    }

    window.addEventListener("scroll", handleScroll)

    return () => {
      window.removeEventListener("scroll", handleScroll)
    }
  }, [])

  return (
    <div class="item" style={{'--scrollY': `${Math.min(0, scrollY/3 - 60)}px`}}>
      Item
    </div>
  )
}
Calderón
quelle
Ich bin neugierig auf die useLayoutEffect. Ich versuche zu sehen, was Sie erwähnt haben.
Giovannipds
Wenn es Ihnen nichts ausmacht, können Sie uns bitte ein Repo + visuelles Beispiel dafür geben? Ich konnte einfach nicht reproduzieren, was Sie useEffecthier im Vergleich zu als Problem erwähnt haben useLayoutEffect.
Giovannipds
Sicher! https://github.com/calderon/uselayout-vs-uselayouteffect . Das ist mir erst gestern mit einem ähnlichen Verhalten passiert. Übrigens, ich bin ein reaktionsfreudiger Neuling und möglicherweise irre ich mich völlig: D: D
Calderón
Eigentlich habe ich das schon oft versucht, viel neu geladen , und manchmal erscheint der Header in Rot statt in Blau, was bedeutet, dass .Header--scrolledmanchmal eine Klasse .Header--scrolledLayoutangewendet wird, aber dank useLayoutEffect wird 100% richtig angewendet.
Calderón
Ich habe das Repo auf github.com/calderon/useeffect-vs-uselayouteffect
Calderón
1

Hier ist ein weiteres Beispiel mit HOOKS fontAwesomeIcon und Kendo UI React
[! [Screenshot hier] [1]] [1]

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';


const ScrollBackToTop = () => {
  const [show, handleShow] = useState(false);

  useEffect(() => {
    window.addEventListener('scroll', () => {
      if (window.scrollY > 1200) {
        handleShow(true);
      } else handleShow(false);
    });
    return () => {
      window.removeEventListener('scroll');
    };
  }, []);

  const backToTop = () => {
    window.scroll({ top: 0, behavior: 'smooth' });
  };

  return (
    <div>
      {show && (
      <div className="backToTop text-center">
        <button className="backToTop-btn k-button " onClick={() => backToTop()} >
          <div className="d-none d-xl-block mr-1">Top</div>
          <FontAwesomeIcon icon="chevron-up"/>
        </button>
      </div>
      )}
    </div>
  );
};

export default ScrollBackToTop;```


  [1]: https://i.stack.imgur.com/ZquHI.png
jso1919
quelle
Das ist fantastisch. Ich hatte ein Problem damit, dass useEffect () den Status meines Navigationsleisten-Stickys beim Scrollen mithilfe von window.onscroll () änderte. Durch diese Antwort wurde herausgefunden, dass window.addEventListener () und window.removeEventListener () der richtige Ansatz zur Steuerung meines Sticky sind navbar mit einer funktionalen Komponente ... danke!
Michael
1

Update für eine Antwort mit React Hooks

Dies sind zwei Haken - einer für die Richtung (oben / unten / keine) und einer für die tatsächliche Position

Verwenden Sie wie folgt:

useScrollPosition(position => {
    console.log(position)
  })

useScrollDirection(direction => {
    console.log(direction)
  })

Hier sind die Haken:

import { useState, useEffect } from "react"

export const SCROLL_DIRECTION_DOWN = "SCROLL_DIRECTION_DOWN"
export const SCROLL_DIRECTION_UP = "SCROLL_DIRECTION_UP"
export const SCROLL_DIRECTION_NONE = "SCROLL_DIRECTION_NONE"

export const useScrollDirection = callback => {
  const [lastYPosition, setLastYPosition] = useState(window.pageYOffset)
  const [timer, setTimer] = useState(null)

  const handleScroll = () => {
    if (timer !== null) {
      clearTimeout(timer)
    }
    setTimer(
      setTimeout(function () {
        callback(SCROLL_DIRECTION_NONE)
      }, 150)
    )
    if (window.pageYOffset === lastYPosition) return SCROLL_DIRECTION_NONE

    const direction = (() => {
      return lastYPosition < window.pageYOffset
        ? SCROLL_DIRECTION_DOWN
        : SCROLL_DIRECTION_UP
    })()

    callback(direction)
    setLastYPosition(window.pageYOffset)
  }

  useEffect(() => {
    window.addEventListener("scroll", handleScroll)
    return () => window.removeEventListener("scroll", handleScroll)
  })
}

export const useScrollPosition = callback => {
  const handleScroll = () => {
    callback(window.pageYOffset)
  }

  useEffect(() => {
    window.addEventListener("scroll", handleScroll)
    return () => window.removeEventListener("scroll", handleScroll)
  })
}
Dowi
quelle
0

Um die Antwort von @ Austin zu erweitern, sollten Sie this.handleScroll = this.handleScroll.bind(this)Ihrem Konstruktor Folgendes hinzufügen :

constructor(props){
    this.handleScroll = this.handleScroll.bind(this)
}
componentDidMount: function() {
    window.addEventListener('scroll', this.handleScroll);
},

componentWillUnmount: function() {
    window.removeEventListener('scroll', this.handleScroll);
},

handleScroll: function(event) {
    let scrollTop = event.srcElement.body.scrollTop,
        itemTranslate = Math.min(0, scrollTop/3 - 60);

    this.setState({
      transform: itemTranslate
    });
},
...

Dies gibt handleScroll()Zugriff auf den richtigen Bereich, wenn es vom Ereignis-Listener aufgerufen wird.

Beachten Sie auch, dass Sie .bind(this)die Methoden in addEventListeneroder removeEventListenernicht ausführen können, da sie jeweils Verweise auf verschiedene Funktionen zurückgeben und das Ereignis beim Entfernen der Komponente nicht entfernt wird.

nbwoodward
quelle