Aggiorna lo stile di un componente onScroll in React.js


133

Ho creato un componente in React che dovrebbe aggiornare il suo stile sullo scroll della finestra per creare un effetto di parallasse.

Il rendermetodo del componente è simile al seguente:

  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>
    );
  }

Questo non funziona perché React non sa che il componente è stato modificato, quindi il componente non viene reso nuovamente.

Ho provato a memorizzare il valore di itemTranslatenello stato del componente e a chiamare setStateil callback di scorrimento. Tuttavia, ciò rende lo scorrimento inutilizzabile poiché è terribilmente lento.

Qualche suggerimento su come farlo?


9
Non associare mai un gestore eventi esterno all'interno di un metodo di rendering. I metodi di rendering (e qualsiasi altro metodo personalizzato da cui chiami rendernello stesso thread) dovrebbero riguardare solo la logica relativa al rendering / aggiornamento del DOM effettivo in React. Invece, come mostrato da @AustinGreco di seguito, è necessario utilizzare i metodi del ciclo di vita di React forniti per creare e rimuovere l'associazione di eventi. Ciò lo rende autonomo all'interno del componente e garantisce l'assenza di perdite assicurandosi che l'associazione di eventi sia rimossa se / quando il componente che lo utilizza è smontato.
Mike Driver,

Risposte:


232

Dovresti legare l'ascoltatore componentDidMount, in questo modo viene creato una sola volta. Dovresti essere in grado di memorizzare lo stile nello stato, l'ascoltatore era probabilmente la causa di problemi di prestazioni.

Qualcosa come questo:

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
    });
},

26
Ho scoperto che setState all'interno dell'evento scroll per l'animazione è instabile. Ho dovuto impostare manualmente lo stile dei componenti usando refs.
Ryan Rho,

1
Cosa dovrebbe essere indicato il "this" dentro handleScroll? Nel mio caso è "finestra" non componente. Finisco per passare il componente come parametro
yuji

10
@yuji puoi evitare di dover passare il componente legandolo nel costruttore: this.handleScroll = this.handleScroll.bind(this)lo legherà handleScrollal componente, anziché alla finestra.
Matt Parrilla

1
Nota che srcElement non è disponibile in Firefox.
Paulin Trognon

2
non ha funzionato per me, ma quello che ha fatto è stato impostare scrollTop suevent.target.scrollingElement.scrollTop
George

31

È possibile passare una funzione onScrollall'evento sull'elemento React: https://facebook.github.io/react/docs/events.html#ui-events

<ScrollableComponent
 onScroll={this.handleScroll}
/>

Un'altra risposta simile: https://stackoverflow.com/a/36207913/1255973


5
C'è qualche vantaggio / svantaggio di questo metodo rispetto all'aggiunta manuale di un listener di eventi all'elemento finestra @AustinGreco menzionato?
Dennis,

2
@Dennis Uno dei vantaggi è che non è necessario aggiungere / rimuovere manualmente i listener di eventi. Mentre questo potrebbe essere un semplice esempio se gestisci manualmente più listener di eventi in tutta la tua applicazione, è facile dimenticare di rimuoverli correttamente durante gli aggiornamenti, il che può portare a bug di memoria. Userei sempre la versione integrata, se possibile.
F Lekschas,

4
Vale la pena notare che questo collega un gestore di scorrimento al componente stesso, non alla finestra, il che è una cosa molto diversa. @Dennis I vantaggi di onScroll sono che è più cross-browser e più performante. Se puoi usarlo probabilmente dovresti, ma potrebbe non essere utile in casi come quello per l'OP
Beau

20

La mia soluzione per creare una barra di navigazione reattiva (posizione: 'relativo' quando non si scorre e fisso durante lo scorrimento e non nella parte superiore della pagina)

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

Nessun problema di prestazioni per me.


Puoi anche usare un'intestazione falsa che essenzialmente è solo un segnaposto. Quindi hai la tua intestazione fissa e sotto di essa hai il tuo segnaposto segnaposto con posizione: relativo.
robins_

Nessun problema di prestazioni perché questo non affronta la sfida di parallasse nella domanda.
juanitogan,

19

per aiutare chiunque qui abbia notato problemi di comportamento / prestazioni ritardati quando si utilizza la risposta di Austins e desidera un esempio utilizzando gli ref citati nei commenti, ecco un esempio che stavo usando per attivare / disattivare una classe per un'icona di scorrimento su / giù:

Nel metodo di rendering:

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

Nel metodo del gestore:

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');
  }
}

E aggiungi / rimuovi i tuoi gestori nello stesso modo di cui parla Austin:

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

documenti sugli arbitri .


4
Mi hai salvato la giornata! Per l'aggiornamento, in realtà non è necessario utilizzare jquery per modificare il nome classe a questo punto, poiché è già un elemento DOM nativo. Quindi potresti semplicemente farlo this.scrollIcon.className = whatever-you-want.
southp

2
questa soluzione rompe l'incapsulamento di React anche se non sono ancora sicuro di un modo per aggirare questo problema senza un comportamento lento - forse un evento di scroll rimbalzato (forse 200-250 ms) sarebbe una soluzione qui
Giordania

nessun evento di scorrimento debounce aiuta solo a rendere lo scorrimento più fluido (in senso non bloccante), ma sono necessari 500 ms al secondo affinché gli aggiornamenti dichiarino applicarsi nel DOM: /
Giordania

Ho usato anche questa soluzione, +1. Sono d'accordo che non hai bisogno di jQuery: basta usare classNameo classList. Inoltre, non ne avevo bisognowindow.addEventListener() : ho appena usato React's onScroll, ed è veloce, purché non aggiorni oggetti di scena / stato!
Benjamin,

13

Ho scoperto che non posso aggiungere correttamente il listener di eventi a meno che non passi true in questo modo:

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

Sta funzionando. Ma riesci a capire perché dobbiamo passare il vero booleano a questo ascoltatore.
Shah Chaitanya,

2
Da w3schools: [ w3schools.com/jsref/met_document_addeventlistener.asp] userCapture : facoltativo. Un valore booleano che specifica se l'evento deve essere eseguito nella fase di acquisizione o di bubbling. Valori possibili: true: il gestore eventi viene eseguito nella fase di acquisizione false: impostazione predefinita. Il gestore dell'evento viene eseguito nella fase gorgogliante
Jean-Marie Dalmasso,

12

Un esempio che usa classNames , React hooks useEffect , useState e 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

1
Si noti che poiché useEffect non ha un secondo argomento, verrà eseguito ogni volta che viene eseguito il rendering dell'intestazione.
Giordania,

2
@Jordan hai ragione! Il mio errore è stato scrivere qui. Modificherò la risposta. Grazie mille.
giovannipds,

8

con ganci

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

function MyApp () {

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

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

  console.log(offset); 
};

Esattamente quello di cui avevo bisogno. Grazie!
aabbccsmith

Questa è di gran lunga la risposta più efficace ed elegante di tutte. Grazie per questo.
Chigozie Orunta,

Ciò richiede più attenzione, perfetto.
Anders Kitson,

6

Esempio di componente di funzione che utilizza useEffect:

Nota : è necessario rimuovere il listener di eventi restituendo una funzione di "pulizia" in useEffect. In caso contrario, ogni volta che il componente si aggiorna avrai un listener di scorrimento delle finestre aggiuntivo.

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>
  );
}

Si noti che poiché useEffect non ha un secondo argomento, verrà eseguito ogni volta che viene eseguito il rendering del componente.
Giordania,

Buon punto. Dovremmo aggiungere un array vuoto alla chiamata useEffect?
Richard,

Questo è quello che vorrei fare :)
Giordania,

5

Se quello che ti interessa è un componente figlio che sta scorrendo, questo esempio potrebbe essere di aiuto: 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>
    )
  }
}

1

Ho risolto il problema utilizzando e modificando le variabili CSS. In questo modo non è necessario modificare lo stato del componente che causa problemi di prestazioni.

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);
}

1

La mia scommessa qui è usare i componenti Function con nuovi hook per risolverlo, ma invece di usare useEffectcome nelle risposte precedenti, penso che l'opzione corretta sarebbe useLayoutEffectper un motivo importante:

La firma è identica a useEffect, ma si attiva in modo sincrono dopo tutte le mutazioni DOM.

Questo può essere trovato nella documentazione di React . Se useEffectinvece utilizziamo e ricarichiamo la pagina già scorsa, lo scorrimento sarà falso e la nostra classe non verrà applicata, causando un comportamento indesiderato.

Un esempio:

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

Una possibile soluzione al problema potrebbe essere https://codepen.io/dcalderon/pen/mdJzOYq

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

Sono curioso del useLayoutEffect. Sto cercando di vedere di cosa hai parlato.
giovannipds

Se non ti dispiace, potresti fornire un repository + un esempio visivo di ciò che sta accadendo? Non riuscivo a riprodurre ciò che hai menzionato come un problema di useEffectqui rispetto a useLayoutEffect.
giovannipds,

Sicuro! https://github.com/calderon/uselayout-vs-uselayouteffect . Questo mi è successo proprio ieri con un comportamento simile. A proposito, sono un principiante reagire e forse mi sbaglio totalmente: D: D
Calderón

In realtà ci ho provato molte volte, ricaricando molto, e a volte appare un'intestazione in rosso anziché in blu, il che significa che a .Header--scrolledvolte applica la classe, ma un 100% delle volte .Header--scrolledLayoutviene applicato correttamente grazie a useLayoutEffect.
Calderón,


1

Ecco un altro esempio di utilizzo GANCI fontAwesomeIcon e Kendo UI Reagire
[! [Screenshot qui] [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

Questo e spettacolare. Ho avuto un problema nel mio useEffect () cambiando lo stato del mio stick della barra di navigazione su scroll usando window.onscroll () ... ho scoperto attraverso questa risposta che window.addEventListener () e window.removeEventListener () sono l'approccio giusto per controllare il mio sticky barra di navigazione con un componente funzionale ... grazie!
Michael,

1

Aggiornamento per una risposta con React Hooks

Questi sono due ganci: uno per la direzione (su / giù / nessuno) e uno per la posizione effettiva

Usa così:

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

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

Ecco i ganci:

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

0

Per espandere la risposta di @ Austin, è necessario aggiungere this.handleScroll = this.handleScroll.bind(this)al costruttore:

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
    });
},
...

Questo da handleScroll() accedere all'ambito corretto quando viene chiamato dal listener di eventi.

Anche essere consapevoli non si può fare la .bind(this)nei addEventListenero removeEventListenermetodi, perché ciascuno di essi i riferimenti di ritorno a diverse funzioni e l'evento non verranno rimossi quando lo smonta componenti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.