Zaktualizuj styl komponentu onScroll w React.js


133

Zbudowałem komponent w Reakcie, który ma aktualizować swój własny styl przy przewijaniu okna, aby stworzyć efekt paralaksy.

Metoda komponentu renderwygląda następująco:

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

To nie działa, ponieważ React nie wie, że komponent się zmienił i dlatego komponent nie jest ponownie renderowany.

Próbowałem zapisać wartość itemTranslatew stanie komponentu i wywołać setStatewywołanie zwrotne przewijania. Jednak to sprawia, że ​​przewijanie jest bezużyteczne, ponieważ jest strasznie wolne.

Jakieś sugestie, jak to zrobić?


9
Nigdy nie wiąż zewnętrznej procedury obsługi zdarzeń wewnątrz metody renderowania. Metody renderowania (i wszelkie inne niestandardowe metody, z których wywołujesz renderw tym samym wątku) powinny zajmować się tylko logiką związaną z renderowaniem / aktualizowaniem rzeczywistego DOM w React. Zamiast tego, jak pokazano poniżej @AustinGreco, powinieneś użyć podanych metod cyklu życia Reacta, aby utworzyć i usunąć powiązanie zdarzenia. Dzięki temu jest on samowystarczalny wewnątrz składnika i zapewnia brak wycieków, upewniając się, że powiązanie zdarzenia zostanie usunięte, jeśli / kiedy składnik, który go używa, zostanie odmontowany.
— Mike Driver

Odpowiedzi:


232

Powinieneś związać słuchacza, w componentDidMountten sposób zostanie utworzony tylko raz. Powinieneś być w stanie przechowywać styl w stanie, prawdopodobnie słuchacz był przyczyną problemów z wydajnością.

Coś takiego:

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
Zauważyłem, że zdarzenie setState'ing inside scroll dla animacji jest przerywane. Musiałem ręcznie ustawić styl komponentów za pomocą ref.
— Ryan Rho

1
Na co wskazywałoby „this” inside handleScroll? W moim przypadku jest to „okno”, a nie komponent. W końcu przekazuję komponent jako parametr
— yuji

10
@yuji możesz uniknąć konieczności przekazywania komponentu, wiążąc to w konstruktorze: this.handleScroll = this.handleScroll.bind(this)powiąże to wewnątrz handleScrollkomponentu zamiast okna.
— Matt Parrilla,

1
Zauważ, że srcElement nie jest dostępny w przeglądarce Firefox.
— Paulin Trognon

2
nie zadziałało dla mnie, ale ustawiłem scrollTop naevent.target.scrollingElement.scrollTop
— George

31

Możesz przekazać funkcję do onScrollzdarzenia w elemencie React: https://facebook.github.io/react/docs/events.html#ui-events

<ScrollableComponent
 onScroll={this.handleScroll}
/>

Inna odpowiedź, która jest podobna: https://stackoverflow.com/a/36207913/1255973


5
Czy jest jakaś korzyść / wada tej metody w porównaniu z ręcznym dodawaniem detektora zdarzeń do wspomnianego elementu window @AustinGreco?
— Dennis

2
@Dennis Jedną z korzyści jest to, że nie musisz ręcznie dodawać / usuwać detektorów zdarzeń. Chociaż może to być prosty przykład, jeśli ręcznie zarządzasz kilkoma odbiornikami zdarzeń w całej aplikacji, łatwo zapomnieć o ich poprawnym usunięciu podczas aktualizacji, co może prowadzić do błędów pamięci. Jeśli to możliwe, zawsze korzystałbym z wersji wbudowanej.
— F Lekschas

4
Warto zauważyć, że dołącza to moduł obsługi przewijania do samego komponentu, a nie do okna, co jest zupełnie inną rzeczą. @Dennis Zalety onScroll polegają na tym, że działa on bardziej w różnych przeglądarkach i jest bardziej wydajny. Jeśli możesz go użyć, prawdopodobnie powinieneś, ale może nie być przydatny w przypadkach takich jak ten dla OP
— Beau

20

Moje rozwiązanie do tworzenia responsywnego paska nawigacyjnego (pozycja: „względna”, gdy nie jest przewijana i ustalona podczas przewijania, a nie na górze strony)

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

Żadnych problemów z wydajnością.


Możesz także użyć fałszywego nagłówka, który zasadniczo jest tylko symbolem zastępczym. Masz więc swój stały nagłówek, a pod nim masz zastępczy fałszywy nagłówek z pozycją: względna.
— robins_

Brak problemów z wydajnością, ponieważ nie rozwiązuje to problemu paralaksy w pytaniu.
— juanitogan

19

aby pomóc każdemu, kto zauważył opóźnione zachowanie / problemy z wydajnością podczas korzystania z odpowiedzi Austins i chciałby uzyskać przykład z odniesieniami wymienionymi w komentarzach, oto przykład, którego używałem do przełączania klasy dla ikony przewijania w górę / w dół:

W metodzie renderowania:

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

W metodzie obsługi:

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

I dodaj / usuń swoje programy obsługi w taki sam sposób, jak wspomniał Austin:

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

dokumenty dotyczące referencji .


4
Uratowałeś mi dzień! Aby zaktualizować, w rzeczywistości nie musisz używać jquery do modyfikowania nazwy klasy w tym momencie, ponieważ jest to już natywny element DOM. Możesz więc po prostu zrobić this.scrollIcon.className = whatever-you-want.
— południe

2
to rozwiązanie psuje hermetyzację React, chociaż nadal nie jestem pewien, jak to obejść bez opóźnionego zachowania - być może zdarzenie przewijania odbicia (może 200-250 ms) byłoby tutaj rozwiązaniem
— Jordan

nie zdarzenie zdejmowanego przewijania tylko pomaga uczynić przewijanie płynniejszym (w sensie nieblokującym), ale aktualizacja stanu do zastosowania w DOM zajmuje 500 ms na sekundę: /
— Jordania

Ja też korzystałem z tego rozwiązania +1. Zgadzam się, że nie potrzebujesz jQuery: po prostu użyj classNamelub classList. Poza tym nie potrzebowałemwindow.addEventListener() : właśnie użyłem Reacta onScrolli jest tak szybki, o ile nie aktualizujesz właściwości / stanu!
— Benjamin,

13

Okazało się, że nie mogę pomyślnie dodać detektora zdarzeń, chyba że przekażę prawdę w następujący sposób:

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

To działa. Ale czy możesz zrozumieć, dlaczego musimy przekazać temu słuchaczowi prawdziwe wartości logiczne.
— shah chaitanya,

2
Z w3schools: [ w3schools.com/jsref/met_document_addeventlistener.asp] userCapture : opcjonalnie. Wartość logiczna, która określa, czy zdarzenie powinno zostać wykonane w fazie przechwytywania, czy w fazie propagacji. Możliwe wartości: true - procedura obsługi zdarzenia jest wykonywana w fazie przechwytywania false - wartość domyślna. Procedura obsługi zdarzenia jest wykonywana w fazie bulgotania
— Jean-Marie Dalmasso

12

Przykład z użyciem classNames , haków React useEffect , useState i 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
Zauważ, że ponieważ ten parametr useEffect nie ma drugiego argumentu, będzie uruchamiany za każdym razem, gdy renderuje się nagłówek.
— Jordan

2
@Jordan masz rację! Mój błąd, pisząc to tutaj. Zmienię odpowiedź. Dziękuję Ci bardzo.
— giovannipds

8

z haczykami

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

function MyApp () {

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

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

  console.log(offset); 
};

Dokładnie to, czego potrzebowałem. Dzięki!
— aabbccsmith

To zdecydowanie najbardziej skuteczna i elegancka odpowiedź ze wszystkich. Dzięki za to.
— Chigozie Orunta

To wymaga więcej uwagi, doskonale.
— Anders Kitson

6

Przykład składnika funkcji z użyciem useEffect:

Uwaga : należy usunąć detektor zdarzeń, zwracając funkcję „czyszczenia” w useEffect. Jeśli tego nie zrobisz, za każdym razem, gdy składnik zostanie zaktualizowany, będziesz mieć dodatkowy odbiornik przewijania okien.

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

Zwróć uwagę, że ponieważ ten parametr useEffect nie ma drugiego argumentu, będzie uruchamiany za każdym razem, gdy komponent będzie renderowany.
— Jordan

Słuszna uwaga. Czy powinniśmy dodać pustą tablicę do wywołania useEffect?
— Richard

To właśnie bym zrobił :)
— Jordan

5

Jeśli interesuje Cię składnik podrzędny, który przewija się, ten przykład może być pomocny: 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

Rozwiązałem problem, używając i modyfikując zmienne CSS. W ten sposób nie muszę modyfikować stanu komponentu, który powoduje problemy z wydajnością.

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

Mój zakład tutaj polega na używaniu komponentów funkcji z nowymi hakami do rozwiązania tego problemu, ale zamiast używać useEffectjak w poprzednich odpowiedziach, myślę, że poprawna opcja byłaby useLayoutEffectz ważnego powodu:

Sygnatura jest identyczna jak useEffect, ale jest uruchamiana synchronicznie po wszystkich mutacjach DOM.

Można to znaleźć w dokumentacji Reacta . Jeśli useEffectzamiast tego użyjemy i ponownie załadujemy stronę już przewiniętą, scrolled będzie fałszywe, a nasza klasa nie zostanie zastosowana, powodując niepożądane zachowanie.

Przykład:

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

Możliwym rozwiązaniem problemu mogłoby być 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>
  )
}

Ciekawi mnie useLayoutEffect. Próbuję zobaczyć, o czym wspomniałeś.
— giovannipds

Jeśli nie masz nic przeciwko, czy możesz podać repozytorium + wizualny przykład tego wydarzenia? Po prostu nie mogłem odtworzyć tego, o czym wspomniałeś jako problem useEffectw porównaniu z useLayoutEffect.
— giovannipds

Pewnie! https://github.com/calderon/uselayout-vs-uselayouteffect . Przydarzyło mi się to wczoraj z podobnym zachowaniem. Przy okazji, jestem nowicjuszem i prawdopodobnie całkowicie się mylę: D: D
— Calderón

Właściwie próbowałem tego wiele razy, często przeładowując, a czasami pojawia się nagłówek w kolorze czerwonym zamiast niebieskiego, co oznacza, że .Header--scrolledczasami stosuje klasę, ale 100% razy .Header--scrolledLayoutjest poprawnie stosowane dzięki useLayoutEffect.
— Calderón


1

Oto kolejny przykład za pomocą haków fontAwesomeIcon i Kendo UI React
[! [Screenshot tutaj] [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

To jest niesamowite. Miałem problem z useEffect (), zmieniając stan paska nawigacyjnego przy przewijaniu za pomocą window.onscroll () ... dzięki tej odpowiedzi dowiedziałem się, że window.addEventListener () i window.removeEventListener () są właściwym podejściem do kontrolowania mojego sticky pasek nawigacyjny z elementem funkcjonalnym ... dzięki!
— Michael

1

Zaktualizuj odpowiedź z React Hooks

Są to dwa haczyki - jeden dla kierunku (góra / dół / brak) i jeden dla aktualnej pozycji

Użyj w ten sposób:

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

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

Oto haczyki:

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

Aby rozwinąć odpowiedź @ Austina, powinieneś dodać this.handleScroll = this.handleScroll.bind(this)do swojego konstruktora:

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

To daje handleScroll() dostęp do właściwego zakresu po wywołaniu z detektora zdarzeń.

Należy również pamiętać, że nie można wykonać metody .bind(this)w metodach addEventListenerlub removeEventListener, ponieważ każda z nich zwróci odwołania do różnych funkcji, a zdarzenie nie zostanie usunięte po odłączeniu komponentu.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.