React.js'de onScroll bileşeninin stilini güncelleme


133

React'te, bir paralaks efekti oluşturmak için pencere kaydırmasında kendi stilini güncellemesi gereken bir bileşen oluşturdum.

Bileşen renderyöntemi şuna benzer:

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

Bu işe yaramaz çünkü React bileşenin değiştiğini bilmiyor ve bu nedenle bileşen yeniden oluşturulmuyor.

Değerini itemTranslatebileşenin durumunda depolamayı setStateve kaydırma geri aramasında çağırmayı denedim . Ancak bu, son derece yavaş olduğu için kaydırmayı kullanılamaz hale getirir.

Bunun nasıl yapılacağına dair herhangi bir öneriniz var mı?


9
Bir oluşturma yönteminin içine asla harici bir olay işleyicisini bağlamayın. Oluşturma yöntemleri (ve renderaynı iş parçacığı içinde çağırdığınız diğer tüm özel yöntemler ) yalnızca React'te gerçek DOM'un oluşturulması / güncellenmesiyle ilgili mantıkla ilgilenmelidir. Bunun yerine, aşağıda @AustinGreco tarafından gösterildiği gibi, olay bağlamanızı oluşturmak ve kaldırmak için verilen React yaşam döngüsü yöntemlerini kullanmalısınız. Bu, bileşenin içinde bağımsız olmasını sağlar ve onu kullanan bileşen çıkarıldığında / olduğunda olay bağlamasının kaldırıldığından emin olarak sızıntı olmamasını sağlar.
Mike Driver

Yanıtlar:


232

Dinleyiciyi bağlamanız gerekir componentDidMount, bu şekilde yalnızca bir kez oluşturulur. Stili durumda saklayabilmelisiniz, muhtemelen performans sorunlarının nedeni dinleyiciydi.

Bunun gibi bir şey:

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
Animasyon için kaydırma olayının içindeki setState'in değişken olduğunu buldum. Referansları kullanarak bileşenlerin stilini manuel olarak ayarlamak zorunda kaldım.
Ryan Rho

1
HandleScroll'un içindeki "bu" neyi gösterir? Benim durumumda bu "pencere" bileşeni değil. Bileşeni bir parametre olarak
geçiriyorum

10
@yuji, bunu yapıcıda bağlayarak bileşeni iletme ihtiyacından kaçınabilirsiniz: this.handleScroll = this.handleScroll.bind(this)bunu handleScrollpencere yerine bileşene bağlar .
Matt Parrilla

1
SrcElement'in Firefox'ta bulunmadığını unutmayın.
Paulin Trognon

2
benim için işe yaramadı, ama yapılan kaydırma ayarını yapmaktı Top toevent.target.scrollingElement.scrollTop
George

31

onScrollReact öğesindeki etkinliğe bir işlev iletebilirsiniz : https://facebook.github.io/react/docs/events.html#ui-events

<ScrollableComponent
 onScroll={this.handleScroll}
/>

Benzer başka bir cevap: https://stackoverflow.com/a/36207913/1255973


5
@AustinGreco'nun bahsettiği pencere öğesine manuel olarak bir olay dinleyicisi eklemeye karşı bu yöntemin herhangi bir avantajı / sakıncası var mı?
Dennis

2
@Dennis'in bir avantajı, olay dinleyicilerini manuel olarak eklemenize / kaldırmanıza gerek olmamasıdır. Bu basit bir örnek olsa da, uygulamanızın her yerinde birden fazla olay dinleyicisini manuel olarak yönetirseniz, güncellemeler üzerine bunları düzgün bir şekilde kaldırmayı unutmak kolaydır, bu da bellek hatalarına yol açabilir. Mümkünse her zaman yerleşik sürümü kullanırdım.
F Lekschas

4
Bunun pencereye değil, bileşenin kendisine bir kaydırma işleyicisi eklediğini belirtmek gerekir ki bu çok farklı bir şeydir. @Dennis onScroll'un faydaları, daha çapraz tarayıcı ve daha performanslı olmasıdır. Kullanabiliyorsanız, muhtemelen kullanmalısınız, ancak OP için olduğu gibi durumlarda yararlı olmayabilir
Beau

20

Duyarlı bir gezinme çubuğu oluşturma çözümüm (konum: kaydırılmadığında 'göreli' ve kaydırırken sabit ve sayfanın üst kısmında değil)

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

Benim için performans sorunu yok.


Ayrıca, aslında sadece bir yer tutucu olan sahte bir başlık da kullanabilirsiniz. Böylece sabit başlığınız var ve bunun altında yer tutucu fakeheader ve konum: göreli var.
robins_

Performans sorunu yok çünkü bu, sorudaki paralaks sorununu ele almıyor.
juanitogan

19

Austins cevabını kullanırken gecikmeli davranış / performans sorunlarını fark eden ve yorumlarda bahsedilen referansları kullanarak bir örnek isteyen herkese yardımcı olmak için, bir sınıfı yukarı / aşağı kaydırma simgesi için değiştirmek için kullandığım bir örnek:

Render yönteminde:

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

İşleyici yönteminde:

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

Ve işleyicilerinizi Austin'in bahsettiği şekilde ekleyin / kaldırın:

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

referanslarla ilgili belgeler .


4
Günümü kurtardın! Güncelleme için, bu noktada sınıf adını değiştirmek için jquery kullanmanıza gerek yoktur, çünkü zaten yerel bir DOM öğesidir. Yani basitçe yapabilirsin this.scrollIcon.className = whatever-you-want.
southp

2
bu çözüm React kapsüllemesini bozuyor, ancak yine de gecikmeli davranış olmadan bunu aşmanın bir yolunu bulamıyorum - belki de geçersiz bir kaydırma olayı (belki 200-250 ms'de) burada bir çözüm olabilir
Jordan

hayır iptal edilmiş kaydırma olayı yalnızca kaydırmayı daha yumuşak hale getirmeye yardımcı olur (engellemesiz bir anlamda), ancak güncellemelerin DOM'da uygulanması 500 ms ila bir saniye sürer: /
Ürdün

Ben de bu çözümü kullandım +1. JQuery'ye ihtiyacınız olmadığını kabul ediyorum: sadece classNameveya kullanın classList. Ayrıca ihtiyacım yoktuwindow.addEventListener() : Sadece React'i kullandım onScrollve props / state'i güncellemediğiniz sürece bu kadar hızlı!
Benjamin

13

Doğruyu şu şekilde geçmedikçe olay dinleyicisini başarıyla ekleyemeyeceğimi anladım:

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

İşe yarıyor. Ama bu dinleyiciye neden gerçek mantıksal değer aktarmamız gerektiğini anlayabilir misiniz?
shah chaitanya

2
W3schools'tan: [ w3schools.com/jsref/met_document_addeventlistener.asp] userCapture : İsteğe bağlı. Olayın yakalama aşamasında mı yoksa köpürme aşamasında mı yürütüleceğini belirten bir Boolean değeri. Olası değerler: true - Olay işleyici, yakalama aşamasında false - Default çalıştırılır. Olay işleyicisi köpürme aşamasında yürütülür
Jean-Marie Dalmasso

12

Kullanıldığı bir örnek classnames , tepki kancalar useEffect , USESTATE ve tarz-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
Bu useEffect'in ikinci bir argümanı olmadığı için, Header her işlendiğinde çalışacağını unutmayın.
Ürdün

2
@ Jordan haklısın! Bunu buraya yazarken benim hatam. Cevabı düzenleyeceğim. Çok teşekkür ederim.
giovannipds

8

kancalı

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

function MyApp () {

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

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

  console.log(offset); 
};

Tam olarak ihtiyacım olan şey. Teşekkürler!
aabbccsmith

Bu, en etkili ve en zarif cevaptır. Bunun için teşekkürler.
Chigozie Orunta

Bu daha fazla dikkat gerektiriyor, mükemmel.
Anders Kitson

6

UseEffect kullanarak işlev bileşeni örneği:

Not : useEffect'te bir "temizleme" işlevi döndürerek olay dinleyicisini kaldırmanız gerekir. Yapmazsanız, bileşen her güncellenişinde ek bir pencere kaydırma dinleyicisine sahip olacaksınız.

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

Bu useEffect'in ikinci bir argümanı olmadığından, bileşen her render edildiğinde çalışacağını unutmayın.
Ürdün

İyi bir nokta. UseEffect çağrısına boş bir dizi eklemeli miyiz?
Richard

Ben de öyle yapardım :)
Ürdün

5

İlgilendiğiniz şey, kaydırılan bir alt bileşense, bu örnek yardımcı olabilir: 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>
    )
  }
}

2

Buradaki bahsim, çözmek için yeni kancalara sahip Fonksiyon bileşenlerini kullanmak, ancak useEffectönceki cevaplarda olduğu gibi kullanmak yerine , doğru seçeneğin useLayoutEffectönemli bir nedenden dolayı olacağını düşünüyorum :

İmza, useEffect ile aynıdır, ancak tüm DOM mutasyonlarından sonra senkronize olarak tetiklenir.

Bu, React belgelerinde bulunabilir . Kullandığımız takdirde useEffectyerine biz sayfasını zaten kaydırılan yanlış olacaktır kaydırılan ve bizim sınıf istenmeyen davranışa neden uygulanmayacaktır yeniden yükleyin.

Bir örnek:

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

Soruna olası bir çözüm https://codepen.io/dcalderon/pen/mdJzOYq olabilir.

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

Ben merak ediyorum useLayoutEffect. Bahsettiğiniz şeyi görmeye çalışıyorum.
giovannipds

Sakıncası yoksa, lütfen bunun repo + görsel bir örneğini verebilir misiniz? useEffectBurada bahsettiğiniz meseleyi kıyaslayarak çoğaltamadım useLayoutEffect.
giovannipds

Elbette! https://github.com/calderon/uselayout-vs-uselayouteffect . Bu, daha dün benzer bir davranışla başıma geldi. BTW, react acemiyim ve muhtemelen tamamen yanılıyorum: D: D
Calderon

Aslında bunu birçok kez deniyorum, çok fazla yeniden yüklüyorum ve bazen başlık mavi yerine kırmızı görünüyor, bu da .Header--scrolledbazen sınıf uyguladığı anlamına geliyor , ancak .Header--scrolledLayoutuseLayoutEffect sayesinde % 100 doğru bir şekilde uygulanıyor.
Calderón


1

CSS değişkenlerini kullanarak ve değiştirerek sorunu çözdüm. Bu şekilde, performans sorunlarına neden olan bileşen durumunu değiştirmek zorunda kalmayacağım.

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

İşte HOOKS fontAwesomeIcon ve Kendo UI React kullanan başka bir örnek
[! [Ekran görüntüsü] [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

Bu harika. Window.onscroll () kullanarak kaydırmada navbar yapışkanımın durumunu değiştirirken useEffect () ile ilgili bir sorun yaşadım ... bu yanıt aracılığıyla window.addEventListener () ve window.removeEventListener () öğelerinin yapışkanımı kontrol etmek için doğru yaklaşım olduğunu öğrendim işlevsel bir bileşene sahip navbar ... teşekkürler!
Michael

1

React Hooks ile bir cevap için güncelleme

Bunlar iki kancadır - biri yön için (yukarı / aşağı / yok) ve diğeri gerçek konum için

Bunun gibi kullanın:

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

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

İşte kancalar:

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

@ Austin'in cevabını genişletmek için kurucunuza eklemelisiniz this.handleScroll = this.handleScroll.bind(this):

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

Bu handleScroll(), olay dinleyicisinden çağrıldığında uygun kapsama erişim sağlar .

Ayrıca .bind(this), addEventListenerveya removeEventListeneryöntemlerinde bunu yapamayacağınızı unutmayın, çünkü her biri farklı işlevlere referanslar döndürecektir ve bileşen ayrıldığında olay kaldırılmayacaktır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.