Обновить стиль компонента 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 внутри события прокрутки для анимации прерывисто. Мне пришлось вручную установить стиль компонентов с помощью ссылок.
Райан Ро
1
На что будет указывать "this" внутри handleScroll? В моем случае это «окно», а не компонент. Я передаю компонент в качестве параметра
юдзи
10
@yuji, вы можете избежать необходимости передавать компонент, привязав его в конструкторе: this.handleScroll = this.handleScroll.bind(this)свяжет это внутри handleScrollс компонентом, а не с окном.
Мэтт Паррилья
1
Обратите внимание, что srcElement недоступен в Firefox.
Paulin Trognon
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 Lekschas
4
Стоит отметить, что это прикрепляет обработчик прокрутки к самому компоненту, а не к окну, что совсем другое. @Dennis Преимущества onScroll в том, что он более кроссбраузерный и более производительный. Если вы можете использовать его, вам, вероятно, следует, но он может быть бесполезен в таких случаях, как тот, что был для OP
Beau
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_
источник
Вы также можете использовать поддельный заголовок, который по сути является просто заполнителем. Итак, у вас есть фиксированный заголовок, а под ним - фальшивый заголовок-заполнитель с position: relative.
robins_
Нет проблем с производительностью, потому что это не решает проблему параллакса в вопросе.
juanitogan
19

Чтобы помочь всем, кто заметил проблемы с медленным поведением / производительностью при использовании ответа Austins и хочет получить пример с использованием ссылок, упомянутых в комментариях, вот пример, который я использовал для переключения класса для значка прокрутки вверх / вниз:

В методе рендеринга:

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

документы по исх .

adrian_reimer
источник
4
Ты спас мне день! Для обновления вам фактически не нужно использовать jquery для изменения имени класса на этом этапе, потому что это уже собственный элемент DOM. Так что вы могли просто сделать this.scrollIcon.className = whatever-you-want.
Southp 09
2
это решение нарушает инкапсуляцию React, хотя я до сих пор не уверен, как обойти это без запаздывания - возможно, здесь решением будет отклоненное событие прокрутки (может быть, 200-250 мс)
Джордан
nope debounced scroll event помогает только сделать прокрутку более плавной (в неблокирующем смысле), но для применения обновлений к состоянию в DOM требуется от 500 мс до секунды: /
Jordan
Я тоже использовал это решение, +1. Я согласен, что вам не нужен jQuery: просто используйте classNameили classList. Кроме того, мне это не понадобилосьwindow.addEventListener() : я просто использовал React onScroll, и это так же быстро, пока вы не обновляете реквизиты / состояние!
Бенджамин
13

Я обнаружил, что не могу успешно добавить прослушиватель событий, если не передаю true следующим образом:

componentDidMount = () => {
    window.addEventListener('scroll', this.handleScroll, true);
},
Жан-Мари Дальмассо
источник
Это работает. Но можете ли вы понять, почему мы должны передать этому слушателю истинное логическое значение.
шах чайтанья
2
Из w3schools: [ w3schools.com/jsref/met_document_addeventlistener.asp] userCapture : необязательно. Логическое значение, указывающее, должно ли событие выполняться в фазе захвата или в фазе восходящей цепочки. Возможные значения: true - обработчик событий выполняется на этапе захвата. False - по умолчанию. Обработчик событий выполняется в фазе
Жан-Мари Дальмассо
12

Пример использования classNames , перехватчиков React useEffect , useState и 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
источник
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
Это, безусловно, самый эффективный и элегантный ответ из всех. Спасибо за это.
Чигози Орунта,
Это требует большего внимания, идеально.
Андерс Китсон
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>
    )
  }
}
user2312410
источник
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);
}
webpreneur
источник
1

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

Подпись идентична useEffect, но срабатывает синхронно после всех мутаций DOM.

Это можно найти в документации React . Если мы используемuseEffect вместо этого и перезагружаем уже прокрученную страницу, scrolled будет ложным, и наш класс не будет применяться, что приведет к нежелательному поведению.

Пример:

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
Кальдерон
На самом деле я пробовал это много раз, много перезагружал, и иногда заголовок отображается красным, а не синим, что означает, что он применяет .Header--scrolled иногда класс, но 100% раз .Header--scrolledLayoutприменяется правильно благодаря useLayoutEffect.
Кальдерон,
Я переместил репо на github.com/calderon/useeffect-vs-uselayouteffect
Кальдерон
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
jso1919
источник
Это круто. У меня возникла проблема с использованием useEffect () с изменением состояния липкости моей навигационной панели при прокрутке с помощью 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)
  })
}
dowi
источник
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потому что каждый из них будет возвращать ссылки на разные функции, и событие не будет удалено при отключении компонента.

nbwoodward
источник