React.jsのコンポーネントonScrollのスタイルを更新する


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メソッド内で外部イベントハンドラーをバインドしないでください。レンダリングメソッド(およびrender同じスレッドで呼び出す他のカスタムメソッド)は、Reactでの実際のDOMのレンダリング/更新に関連するロジックのみに関係する必要があります。代わりに、以下の@AustinGrecoで示すように、所定のReactライフサイクルメソッドを使用して、イベントバインディングを作成および削除する必要があります。これにより、コンポーネント内で自己完結し、それを使用するコンポーネントがマウント解除された場合にイベントバインディングが確実に削除されるため、リークが発生しなくなります。
マイクドライバー

回答:


232

リスナーはにバインドする必要があります。これによりcomponentDidMount、リスナーは1回だけ作成されます。スタイルを状態に保存できるはずです。おそらくリスナーがパフォーマンスの問題の原因でした。

このようなもの:

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が途切れることがわかりました。refsを使用してコンポーネントのスタイルを手動で設定する必要がありました。
Ryan Rho

1
handleScroll内の「this」は何を指すのでしょうか?私の場合、それはコンポーネントではなく「ウィンドウ」です。コンポーネントをパラメーターとして渡してしまう
yuji

10
@yujiコンストラクタでこれをバインドすることで、コンポーネントを渡す必要を回避できます。これは、ウィンドウではなくコンポーネントthis.handleScroll = this.handleScroll.bind(this)handleScrollにバインドされます。
Matt Parrilla

1
srcElementはFirefoxでは使用できないことに注意してください。
Paulin Trognon

2
私にとってはうまくいきませんでしたが、scrollTopをevent.target.scrollingElement.scrollTop
George

31

onScrollReact要素のイベントに関数を渡すことができます:https : //facebook.github.io/react/docs/events.html#ui-events

<ScrollableComponent
 onScroll={this.handleScroll}
/>

同様の別の答え:https : //stackoverflow.com/a/36207913/1255973


5
このメソッドには、@ AustinGrecoというウィンドウ要素にイベントリスナーを手動で追加するのと比べて、メリットや欠点はありますか?
Dennis

2
@Dennis 1つの利点は、イベントリスナーを手動で追加/削除する必要がないことです。これは、アプリケーション全体で複数のイベントリスナーを手動で管理する場合の簡単な例ですが、更新時にそれらを適切に削除することを忘れがちであり、メモリのバグにつながる可能性があります。可能な場合は、常に組み込みバージョンを使用します。
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}}
        >

パフォーマンスの問題はありません。


基本的に単なるプレースホルダーである偽のヘッダーを使用することもできます。したがって、固定ヘッダーがあり、その下に、position:relativeを含むプレースホルダーの偽ヘッダーがあります。
robins_

これは問題の視差の課題に対応していないため、パフォーマンスの問題はありません。
juanitogan 2018

19

オースティンの回答を使用する際に動作の遅れやパフォーマンスの問題に気づき、コメントに記載されている参照を使用した例が必要な人を助けるために、ここでは、スクロールアップ/ダウンアイコンのクラスを切り替えるために使用した例を示します。

renderメソッドでは:

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

refsのドキュメント。


4
あなたは私の日を救った!更新の場合、jqueryを使用してこの時点でクラス名を変更する必要はありません。これは、すでにネイティブDOM要素であるためです。だから、あなたは単に行うことができますthis.scrollIcon.className = whatever-you-want
2016年

2
この解決策はReactカプセル化を壊しますが、遅延動作なしでこれを回避する方法はまだ
ジョーダン

nope debounced scroll eventはスクロールをよりスムーズにする(非ブロッキングの意味で)だけに役立ちますが、更新が状態をDOMに適用するのに500msから1秒かかります:/
Jordan

このソリューションも使用しました、+ 1。jQueryは必要ないことに同意します:classNameまたはを使用するだけclassListです。また、私は必要としませんでしたwindow.addEventListener()。Reactを使用しただけでonScroll、props / stateを更新しない限り高速です。
ベンジャミン

13

次のようにtrueを渡さない限り、イベントリスナーを正常に追加できないことがわかりました。

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

それは働いています。しかし、真のブール値をこのリスナーに渡さなければならない理由を理解できますか。
shah chaitanya

2
w3schoolsから:[ w3schools.com/jsref/met_document_addeventlistener.asp] userCapture:オプション。イベントをキャプチャ段階で実行するか、バブリング段階で実行するかを指定するブール値。可能な値:true-イベントハンドラーはキャプチャ段階で実行されますfalse-デフォルト。イベントハンドラーはバブリングフェーズで実行されます
Jean-Marie Dalmasso

12

classNames、React を使用する例は、useEffectuseState、および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

1
このuseEffectには2番目の引数がないため、ヘッダーがレンダリングされるたびに実行されます。
ジョーダン

2
@ジョーダン、あなたは正しい!これをここに書いて私の間違い。答えを編集します。どうもありがとうございました。
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

これにはもっと注意が必要です。
Anders Kitson

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には2番目の引数がないため、コンポーネントがレンダリングされるたびに実行されます。
ジョーダン

いい視点ね。空の配列を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代わりにを使用して、既にスクロールされたページを再ロードすると、scrolledはfalseになり、クラスが適用されないため、望ましくない動作が発生します。

例:

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、クラスが適用される場合がありますが、.Header--scrolledLayoutuseLayoutEffectのおかげで100%が正しく適用されます。
カルデロン


1

ここで使用した別の例でHOOKS fontAwesomeIconと剣道UIが反応
[!こちらのスクリーンショット] [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

これは素晴らしいです。私は、useEffect()でwindow.onscroll()を使用してスクロール時にナビゲーションバーのスティッキーの状態を変更する際に問題がありました...機能コンポーネントを備えたナビゲーションバー...ありがとうございます。
マイケル

1

React Hooksによる回答の更新

これらは2つのフックです。1つは方向(上/下/なし)と1つは実際の位置です。

このように使用します:

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)でを実行できないことにも注意してください。これらはそれぞれ異なる関数への参照を返し、コンポーネントがマウント解除されてもイベントは削除されないためです。addEventListenerremoveEventListener

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.