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ć?

Alejandro Pérez
źródło
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
    });
},
Austin Greco
źródło
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

Con Antonakos
źródło
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ą.

robins_
źródło
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 .

adrian_reimer
źródło
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);
},
Jean-Marie Dalmasso
źródło
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
giovannipds
źródło
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); 
};
Sodium United
źródło
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>
  );
}
Richard
źródło
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>
    )
  }
}
user2312410
źródło
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);
}
webrepreneur
źródło
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>
  )
}
Calderón
źródło
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
Przeniosłem repozytorium na github.com/calderon/useeffect-vs-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
jso1919
źródło
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)
  })
}
dowi
źródło
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.

nbwoodward
źródło