Perbarui gaya komponen onScroll di React.js

133

Saya telah membangun komponen di Bereaksi yang seharusnya memperbarui gayanya sendiri pada jendela gulir untuk membuat efek paralaks.

renderMetode komponen terlihat seperti ini:

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

Ini tidak berfungsi karena Bereaksi tidak tahu bahwa komponen telah berubah, dan karena itu komponen tidak dirender ulang.

Saya sudah mencoba menyimpan nilai itemTranslatedalam keadaan komponen, dan memanggil setStatedalam panggilan balik gulir. Namun, ini membuat pengguliran tidak dapat digunakan karena ini sangat lambat.

Ada saran tentang cara melakukan ini?

Alejandro Pérez
sumber
9
Jangan pernah mengikat event handler eksternal di dalam metode render. Metode rendering (dan metode kustom lainnya yang Anda panggil dari renderdalam utas yang sama) hanya harus berkaitan dengan logika yang berkaitan dengan rendering / memperbarui DOM aktual dalam Bereaksi. Sebagai gantinya, seperti yang ditunjukkan oleh @AustinGreco di bawah ini, Anda harus menggunakan metode siklus hidup Bereaksi yang diberikan untuk membuat dan menghapus pengikatan acara Anda. Ini membuatnya mandiri di dalam komponen dan memastikan tidak ada kebocoran dengan memastikan acara mengikat dihapus jika / ketika komponen yang menggunakannya dilepas.
Mike Driver

Jawaban:

232

Anda harus mengikat pendengar componentDidMount, dengan cara itu hanya dibuat sekali. Anda harus bisa menyimpan gaya dalam keadaan, pendengar mungkin adalah penyebab masalah kinerja.

Sesuatu seperti ini:

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
sumber
26
Saya menemukan bahwa setState'ing di dalam acara gulir untuk animasi adalah berombak. Saya harus secara manual mengatur gaya komponen menggunakan referensi.
Ryan Rho
1
Apa yang akan ditunjukkan "handle" dalam handleScroll? Dalam kasus saya itu adalah "jendela" bukan komponen. Saya akhirnya melewati komponen sebagai parameter
yuji
10
@ Yuji Anda dapat menghindari keharusan untuk melewati komponen dengan mengikat ini di konstruktor: this.handleScroll = this.handleScroll.bind(this)akan mengikat ini handleScrollke dalam komponen, bukan jendela.
Matt Parrilla
1
Perhatikan bahwa srcElement tidak tersedia di Firefox.
Paulin Trognon
2
tidak bekerja untuk saya, tetapi apa yang dilakukan adalah mengatur scrollTop keevent.target.scrollingElement.scrollTop
George
31

Anda dapat mengirimkan fungsi ke onScrollacara tersebut pada elemen Bereaksi: https://facebook.github.io/react/docs/events.html#ui-events

<ScrollableComponent
 onScroll={this.handleScroll}
/>

Jawaban lain yang serupa: https://stackoverflow.com/a/36207913/1255973

Con Antonakos
sumber
5
Apakah ada manfaat / kelemahan metode ini vs menambahkan pendengar acara secara manual ke elemen jendela @AustinGreco yang disebutkan?
Dennis
2
@Dennis Satu keuntungan adalah Anda tidak perlu menambahkan / menghapus pendengar acara secara manual. Meskipun ini mungkin contoh sederhana jika Anda mengelola beberapa pendengar acara secara manual di seluruh aplikasi Anda, mudah untuk menghapusnya dengan benar saat pembaruan, yang dapat menyebabkan bug memori. Saya akan selalu menggunakan versi bawaan jika memungkinkan.
F Lekschas
4
Perlu dicatat bahwa ini melampirkan pengendali gulir ke komponen itu sendiri, bukan ke jendela, yang merupakan hal yang sangat berbeda. @ Dennis Manfaat dari onScroll adalah lebih lintas-browser dan lebih banyak performan. Jika Anda dapat menggunakannya Anda mungkin harus, tetapi mungkin tidak berguna dalam kasus-kasus seperti yang untuk OP
Beau
20

Solusi saya untuk membuat navbar responsif (posisi: 'relatif' saat tidak menggulir dan diperbaiki saat menggulir dan tidak di bagian atas halaman)

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

Tidak ada masalah kinerja untuk saya.

robins_
sumber
Anda juga dapat menggunakan tajuk palsu yang pada dasarnya hanyalah pengganti. Jadi, Anda memiliki tajuk tetap dan di bawahnya Anda memiliki kepala penipu palsu dengan posisi: relatif.
robins_
Tidak ada masalah kinerja karena ini tidak mengatasi tantangan paralaks dalam pertanyaan.
juanitogan
19

untuk membantu siapa pun di sini yang memperhatikan masalah perilaku / kinerja yang lamban saat menggunakan jawaban Austin, dan ingin contoh menggunakan referensi yang disebutkan dalam komentar, berikut adalah contoh yang saya gunakan untuk mengubah kelas untuk ikon gulir naik / turun:

Dalam metode render:

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

Dalam metode handler:

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

Dan tambahkan / hapus penangan Anda dengan cara yang sama seperti yang disebutkan Austin:

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

dokumen pada referensi .

adrian_reimer
sumber
4
Kamu menyelamatkan hariku! Untuk memperbarui, Anda sebenarnya tidak perlu menggunakan jquery untuk mengubah nama kelas pada saat ini, karena sudah menjadi elemen DOM asli. Jadi Anda bisa melakukannya this.scrollIcon.className = whatever-you-want.
southp
2
solusi ini memecah Bereaksi enkapsulasi meskipun saya masih tidak yakin dengan cara ini tanpa perilaku yang lamban - mungkin acara gulir yang ditolak (mungkin 200-250 ms) akan menjadi solusi di sini
Jordan
nggak ada acara gulir deboelled hanya membantu membuat pengguliran lebih lancar (dalam arti non-pemblokiran), tetapi butuh 500ms per detik untuk pembaruan untuk menyatakan untuk menerapkan di DOM: /
Jordan
Saya menggunakan solusi ini juga, +1. Saya setuju Anda tidak perlu jQuery: cukup gunakan classNameatau classList. Juga, saya tidak perluwindow.addEventListener() : Saya hanya menggunakan React's onScroll, dan itu secepat, asalkan Anda tidak memperbarui alat peraga / negara!
Benjamin
13

Saya menemukan bahwa saya tidak dapat berhasil menambah pendengar acara kecuali saya benar seperti:

componentDidMount = () => {
    window.addEventListener('scroll', this.handleScroll, true);
},
Jean-Marie Dalmasso
sumber
Bekerja. Tetapi dapatkah Anda memahami mengapa kami harus memberikan boolean yang sebenarnya kepada pendengar ini?
shah chaitanya
2
Dari w3schools: [ w3schools.com/jsref/met_document_addeventlistener.asp] userCapture : Opsional. Nilai Boolean yang menentukan apakah acara harus dieksekusi dalam fase penangkapan atau dalam fase gelembung. Nilai yang mungkin: true - Handler event dieksekusi pada fase penangkapan false- Default. Pengatur acara dieksekusi dalam fase menggelegak
Jean-Marie Dalmasso
12

Contoh menggunakan classNames , React hooks useEffect , useState dan style-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
sumber
1
Perhatikan bahwa karena useEffect ini tidak memiliki argumen kedua, ini akan berjalan setiap kali Header merender.
Jordan
2
@ Jordan kau benar! Kesalahan saya menulis ini di sini. Saya akan mengedit jawabannya. Terima kasih banyak.
giovannipds
8

dengan kait

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

function MyApp () {

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

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

  console.log(offset); 
};
Sodium United
sumber
Apa yang saya butuhkan. Terima kasih!
aabbccsmith
Sejauh ini, inilah jawaban yang paling efektif dan elegan. Terima kasih untuk ini.
Chigozie Orunta
Ini membutuhkan lebih banyak perhatian, sempurna.
Anders Kitson
6

Contoh komponen fungsi menggunakan useEffect:

Catatan : Anda harus menghapus pendengar acara dengan mengembalikan fungsi "bersihkan" di useEffect. Jika tidak, setiap kali komponen diperbarui, Anda akan memiliki pendengar gulir jendela tambahan.

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
sumber
Perhatikan bahwa karena useEffect ini tidak memiliki argumen kedua, ini akan berjalan setiap kali komponen dirender.
Jordan
Poin yang bagus. Haruskah kita menambahkan array kosong ke panggilan useEffect?
Richard
Itulah yang akan saya lakukan :)
Jordan
5

Jika yang Anda minati adalah komponen anak yang sedang bergulir, maka contoh ini mungkin bisa membantu: 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>
    )
  }
}
pengguna2312410
sumber
1

Saya memecahkan masalah melalui menggunakan dan memodifikasi variabel CSS. Dengan cara ini saya tidak perlu mengubah status komponen yang menyebabkan masalah kinerja.

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
sumber
1

Taruhan saya di sini adalah menggunakan komponen Fungsi dengan kait baru untuk menyelesaikannya, tetapi alih-alih menggunakan useEffectseperti pada jawaban sebelumnya, saya pikir pilihan yang benar adalah useLayoutEffectkarena alasan penting:

Tanda tangan identik dengan useEffect, tetapi tanda ini menyala secara serempak setelah semua mutasi DOM.

Ini dapat ditemukan di React dokumentasi . Jika kami menggunakan useEffectdan kami memuat ulang halaman yang sudah digulir, digulir akan salah dan kelas kami tidak akan diterapkan, menyebabkan perilaku yang tidak diinginkan.

Sebuah contoh:

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

Solusi yang mungkin untuk masalah ini adalah 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
sumber
Saya ingin tahu tentang useLayoutEffect. Saya mencoba melihat apa yang Anda sebutkan.
giovannipds
Jika Anda tidak keberatan, bisakah Anda memberikan contoh repo + visual dari kejadian ini? Saya tidak bisa mereproduksi apa yang Anda sebutkan sebagai masalah useEffectdi sini dibandingkan dengan useLayoutEffect.
giovannipds
Tentu! https://github.com/calderon/uselayout-vs-uselayouteffect . Ini terjadi pada saya baru kemarin dengan perilaku serupa. BTW, saya seorang pemula yang bereaksi dan mungkin saya benar-benar salah: D: D
Calderón
Sebenarnya saya sudah mencoba ini berkali-kali, memuat ulang banyak, dan kadang-kadang muncul header dalam warna merah, bukan biru, yang berarti .Header--scrolledkadang-kadang menerapkan kelas, tetapi 100% kali .Header--scrolledLayoutditerapkan dengan benar terima kasih useLayoutEffect.
Calderón
Saya memindahkan repo ke github.com/calderon/useeffect-vs-uselayouteffect
Calderón
1

Berikut ini adalah contoh lain menggunakan KAIT fontAwesomeIcon dan Kendo UI Bereaksi
[! [Screenshot di sini] [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
sumber
Ini luar biasa. Saya mempunyai masalah dalam useEffect () saya mengubah status stick navbar saya pada scroll menggunakan window.onscroll () ... menemukan melalui jawaban ini bahwa window.addEventListener () dan window.removeEventListener () adalah pendekatan yang tepat untuk mengendalikan sticky saya navbar dengan komponen fungsional ... terima kasih!
Michael
1

Perbarui jawaban dengan React Hooks

Ini adalah dua kait - satu untuk arah (atas / bawah / tidak ada) dan satu untuk posisi aktual

Gunakan seperti ini:

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

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

Inilah kaitnya:

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
sumber
0

Untuk memperluas jawaban @ Austin, Anda harus menambahkan this.handleScroll = this.handleScroll.bind(this)ke konstruktor Anda:

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

Ini memberi handleScroll() akses ke ruang lingkup yang tepat ketika dipanggil dari pendengar acara.

Ketahuilah bahwa Anda tidak dapat melakukan .bind(this)in addEventListeneratau removeEventListenermetode karena mereka masing-masing akan mengembalikan referensi ke fungsi yang berbeda dan acara tidak akan dihapus ketika komponen dilepas.

nbwoodward
sumber