Оновлення стилю компонента onScroll в React.js


133

Я створив компонент в React, який повинен оновити свій власний стиль у вікні прокрутки, щоб створити ефект паралакса.

Метод компонентів renderвиглядає приблизно так:

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

Це не працює, оскільки React не знає, що компонент змінився, і тому компонент не відтворюється.

Я намагався зберігати значення itemTranslateв стані компонента та викликати setStateзворотний виклик прокрутки. Однак це робить прокрутку непридатною, оскільки це дуже повільно.

Будь-яка пропозиція, як це зробити?


9
Ніколи не зв'язуйте зовнішній обробник подій всередині методу візуалізації. Методи візуалізації (і будь-які інші користувацькі методи, з яких ви телефонуєте renderв тій самій нитці) повинні стосуватися лише логіки, що стосується візуалізації / оновлення фактичного DOM у React. Натомість, як показано на @AustinGreco нижче, слід використовувати задані методи життєвого циклу React для створення та видалення прив'язки подій. Це робить його автономним всередині компонента та забезпечує відсутність протікання, переконуючись, що прив'язка події видалена, якщо / коли компонент, який використовує її, відключений.
Водій Майка

Відповіді:


232

Ви повинні зв’язати слухача componentDidMount , щоб він був створений лише один раз. Ви повинні мати можливість зберігати стиль у стані, слухач, ймовірно, був причиною проблем із виконанням.

Щось на зразок цього:

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
Я виявив, що setState'ing подія прокрутки для анімації хиткий. Мені довелося вручну встановити стиль компонентів за допомогою refs.
Райан Ро

1
На що би вказував "цей" всередині ручкиScroll? У моєму випадку це "вікно" не є компонентом. Я закінчую передачу компонента як параметр
yuji

10
@yuji ви можете уникнути необхідності передавати компонент, прив’язавши це в конструкторі: this.handleScroll = this.handleScroll.bind(this)прив’яже це все handleScrollдо компонента, а не до вікна.
Метт Паррілья

1
Зауважте, що srcElement недоступний у Firefox.
Паулін Трогнон

2
не працювало для мене, але те, що робив, було встановити scrollTop дляevent.target.scrollingElement.scrollTop
Джордж

31

Ви можете передати функцію onScrollподії на елемент React: https://facebook.github.io/react/docs/events.html#ui-events

<ScrollableComponent
 onScroll={this.handleScroll}
/>

Ще одна відповідь, яка схожа: https://stackoverflow.com/a/36207913/1255973


5
Чи є якась користь / недолік цього методу проти вручну додавання слухача події до згаданого елемента вікна @AustinGreco?
Денніс

2
@Dennis Однією з переваг є те, що вам не потрібно вручну додавати / видаляти слухачів подій. Хоча це може бути простим прикладом, якщо ви вручну керуєте кількома слухачами подій у всій програмі, легко забути правильно видалити їх при оновленнях, що може призвести до помилок у пам'яті. Я б завжди використовував вбудовану версію, якщо це можливо.
F Лекшас

4
Варто зазначити, що це прикріплює обробник прокрутки до самого компонента, а не до вікна, що зовсім інша річ. @Dennis Переваги OnScroll полягають у тому, що він більше крос-браузер і ефективніший. Якщо ви можете скористатись ним, напевно, вам слід, але це може бути не корисно у таких випадках, як той для проведення ОП
Бо

20

Моє рішення для створення чуйної навігаційної панелі (позиція: "відносна", коли не прокручується, а виправлена ​​під час прокрутки, а не вгорі сторінки)

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

Немає питань щодо продуктивності для мене.


Ви також можете використовувати підроблений заголовок, який по суті є лише заповнювачем. Отже, ви маєте фіксований заголовок, і під ним ви маєте підроблювач вашого заповнення з позицією: відносний.
robins_

Немає питань щодо ефективності, оскільки це не вирішує проблему паралакса у питанні.
juanitogan

19

щоб допомогти всім, хто помітив лагічні проблеми з поведінкою та працездатністю під час використання відповіді Остінса і хоче приклад з використанням посилань, зазначених у коментарях, ось приклад, який я використовував для перемикання класу на значок прокрутки вгору / вниз:

У методі візуалізації:

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

У способі обробника:

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

І додайте / видаліть ваші обробники так само, як згадував Остін:

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

документи на реф .


4
Ти врятував мій день! Для оновлення вам фактично не потрібно використовувати jquery для зміни імені класу в цей момент, оскільки це вже нативний елемент DOM. Так ви могли просто зробити this.scrollIcon.className = whatever-you-want.
південь

2
це рішення порушує Реакційну інкапсуляцію, хоча я все ще не впевнений у тому, як обійти це без легальної поведінки - можливо, розгорнута подія прокрутки (може бути 200-250 мс) буде рішенням тут
Йорданія

подія прокрутки, яка не відбулася, не допомагає зробити прокрутку плавнішою (у неблокувальному сенсі), але для застосування оновлень, які потрібно застосувати в DOM, потрібно 500 мс в секунду: /
Йорданія,

Я також використав це рішення, +1. Я згоден, вам не потрібен jQuery: просто використовуйте classNameабо classList. Крім того, мені це не потрібноwindow.addEventListener() : я просто використав React onScroll, і це так само швидко, поки ви не оновлюєте реквізит / стан!
Бенджамін

13

Я виявив, що не можу успішно додати слухача події, якщо я не передаю правду так:

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

Це працює. Але чи можете ви зрозуміти, чому ми мусимо передати справжньому булеві цього слухача.
shah chaitanya

2
Від w3schools: [ w3schools.com/jsref/met_document_addeventlistener.asp] userCapture : Необов’язково. Булеве значення, яке вказує, чи слід подія виконуватись у фазі захоплення або у фазі бульбашки. Можливі значення: true - Обробник подій виконується у фазі захоплення false - за замовчуванням. Обробник змагань виконується у фазі
кип'ятіння

12

Приклад з використанням назв класу , React гачки useEffect , useState і стиль-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
Зауважте, що оскільки у цього useEffect немає другого аргументу, він запускатиметься кожного разу, коли заголовка відображається.
Йорданія

2
@Jordan ти маєш рацію! Моя помилка написавши це тут. Я відредагую відповідь. Велике спасибі.
giovannipds

8

з гачками

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

function MyApp () {

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

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

  console.log(offset); 
};

Саме те, що мені було потрібно. Дякую!
aabbccsmith

Це, безумовно, найефективніша та найвишуканіша відповідь з усіх. Дякую за це
Chigozie Orunta

Для цього потрібно більше уваги, досконалості.
Андерс Кітсон

6

Приклад компонента функції за допомогою useEffect:

Примітка . Вам потрібно видалити слухача подій, повернувши функцію "очищення" в useEffect. Якщо цього не зробити, кожен раз, коли компонент оновлюється, у вас з’явиться додатковий слухач прокрутки вікон.

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

Зауважте, що оскільки у цього useEffect немає другого аргументу, він запускатиметься кожного разу, коли компонент відображає.
Йорданія

Гарна думка. Чи слід додати порожній масив до виклику useEffect?
Річард

Ось що б я зробив :)
Йордан

5

Якщо вас цікавить дочірній компонент, який прокручується, то цей приклад може бути корисним: 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

Я вирішив проблему за допомогою використання та зміни змінних CSS. Таким чином, мені не потрібно змінювати стан компонентів, що спричиняє проблеми з продуктивністю.

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

Моя ставка тут полягає у використанні функціональних компонентів з новими гачками для її вирішення, але замість використання, useEffectяк у попередніх відповідях, я думаю, що правильний варіант був би useLayoutEffectважливою причиною:

Підпис ідентичний useEffect, але він запускається синхронно після всіх мутацій DOM.

Це можна знайти в документації React . Якщо ми використовуємо useEffectнатомість і перезавантажимо вже прокручену сторінку, прокрутка буде помилковою, і наш клас не застосовуватиметься, що спричинить небажану поведінку.

Приклад:

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

Можливим рішенням проблеми може стати 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>
  )
}

Мені цікаво про useLayoutEffect. Я намагаюся побачити те, що ви згадали.
giovannipds

Якщо ви не заперечуєте, чи можете ви надати наочний приклад цього репо +? Я просто не міг відтворити те, що ви згадали, як проблему useEffectтут, порівняно з useLayoutEffect.
giovannipds

Звичайно! https://github.com/calderon/uselayout-vs-uselayouteffect . Це сталося зі мною щойно вчора з подібною поведінкою. До речі, я реагую на новачків і, можливо, я абсолютно помиляюся: D: D
Calderón

Насправді я намагався це багато разів, багато перезавантажуючи, а іноді він з’являється заголовком червоного кольору замість синього, а це означає, що він .Header--scrolledіноді застосовує клас, але в 100% разів .Header--scrolledLayoutзастосовується правильно завдяки useLayoutEffect.
Calderón


1

Ось ще один приклад використання Гачки fontAwesomeIcon і Кендо UI React
[! [Скриншот тут] [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

Це круто. У мене виникла проблема в моєму використанніEffect () зміни стану моєї липкої наклейки при прокрутці за допомогою window.onscroll () ... завдяки цій відповіді з'ясувалося, що window.addEventListener () та window.removeEventListener () - це правильний підхід до контролю моєї липкості навбар з функціональним компонентом ... дякую!
Майкл

1

Оновлення відповіді за допомогою React Hooks

Це два гачки - один для напрямку (вгору / вниз / жодного) та один для фактичного положення

Використовуйте так:

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

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

Ось гачки:

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, слід додати 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
    });
},
...

Це дає handleScroll() доступ до належної області, коли дзвонить слухачу події.

Також майте в виду , ви не можете зробити .bind(this)в addEventListenerабо removeEventListenerметодів , оскільки вони будуть кожен поворотні посилання на різні функції і події не будуть видалені , коли компонент демонтує.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.