95

I am trying to implement a List view in React. What I am trying to achieve is that to store the list headers informations and register the components and register the scroll event. every time when user scroll the window, I'd like to take out the stored div and re-calculate the offsetTop data.

The problem now is that, I found the console just print out the initial value (the value is fixed and never changed) offsetTop data never change in onscroll function.

Anyone suggest how to get latest offsetTop from the _instances object?

import React, { Component } from 'react';
import ListHeader from './lib/ListHeader';
import ListItems from './lib/ListItems';

const styles = {
  'height': '400px',
  'overflowY': 'auto',
  'outline': '1px dashed red',
  'width': '40%'
};

class HeaderPosInfo {
  constructor(headerObj, originalPosition, originalHeight) {
    this.headerObj = headerObj;
    this.originalPosition = originalPosition;
    this.originalHeight = originalHeight; 
  }
}

export default class ReactListView extends Component {
  static defaultProps = {
    events: ['scroll', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll', 'resize', 'touchmove', 'touchend'],
    _instances:[],
    _positionMap: new Set(),
    _topPos:'',
    _topWrapper:''
  }

  static propTypes = {
    data: React.PropTypes.array.isRequired,
    headerAttName: React.PropTypes.string.isRequired,
    itemsAttName: React.PropTypes.string.isRequired,
    events: React.PropTypes.array,
    _instances: React.PropTypes.array,
    _positionMap: React.PropTypes.object,
    _topPos: React.PropTypes.string,
    _topWrapper: React.PropTypes.object
  };

  state = {
    events: this.props.events,
    _instances: this.props._instances,
    _positionMap: this.props._positionMap,
    _topPos: this.props._topPos
  }

  componentDidMount() {
    this.initStickyHeaders();
  }

  componentWillUnmount() {

  }

  componentDidUpdate() {

  }

  refsToArray(ctx, prefix){
    let results = [];
    for (let i=0;;i++){
      let ref = ctx.refs[prefix + '-' + String(i)];
      if (ref) results.push(ref);
      else return results;
    }
  }

  initHeaderPositions() {
    // Retrieve all instance of headers and store position info
    this.props._instances.forEach((k)=>{
      this.props._positionMap.add(new HeaderPosInfo(
          k, 
          k.refs.header.getDOMNode().offsetTop,
          k.refs.header.getDOMNode().offsetHeight
        ));
    });
    let it = this.props._positionMap.values();
    let first = it.next();
    this.props._topPos = first.value.originalPosition;
    this.props._topWrapper = first.value.headerObj;
  }

  initStickyHeaders () {
    this.props._instances = this.refsToArray(this, 'ListHeader');
    this.initHeaderPositions();

    // Register events listeners with the listview div
    this.props.events.forEach(type => {
      if (window.addEventListener) {
        React.findDOMNode(this.refs.listview).addEventListener(type, this.onScroll.bind(this), false);
      } else {
        React.findDOMNode(this.refs.listview).attachEvent('on' + type, this.onScroll.bind(this), false);
      }
    });
  }

  onScroll() {

    // update current header positions and apply fixed positions to the top one
    console.log(1);
    let offsetTop  = React.findDOMNode(this.props._instances[0].refs.header).offsetTop;

  }

  render() {
    const { data, headerAttName, itemsAttName } = this.props;
    let _refi = 0;
    let makeRef = () => {
      return 'ListHeader-' + (_refi++);
    };

    return (
      <div ref="listview" style={styles}>
      {
        Object.keys(data).map(k => {
        const header = data[k][headerAttName];
        const items  = data[k][itemsAttName];
          return (
            <ul key={k}>     
              <ListHeader ref={makeRef()} header={header} />
              <ListItems  items={items} />
            </ul>
          );
        })
      }
      </div>
    );
  }
}

The whole source code is on Github, you can clone and compile it from here:

Github

Eric
  • 6,563
  • 5
  • 42
  • 66
JavaScripter
  • 4,282
  • 9
  • 37
  • 45
  • You probably need to add the ```scrollTop``` value to the offsetTop to get the "real" offset. I couldn't get your code on github to work, so I couldn't try it though :/ – Patrick Hübl-Neschkudla Sep 22 '15 at 06:55
  • @PatrickNeschkudla really? what errors do you have? probably you need npm install webpack first. – JavaScripter Sep 22 '15 at 11:49
  • 1
    Just an FYI for everyone that stumbles on this, remember that React has moved `React.findDOMNode` into its own package `react-dom`. See [here](https://facebook.github.io/react/docs/top-level-api.html#reactdom.finddomnode) – aug Sep 07 '16 at 23:09

8 Answers8

116

You may be encouraged to use the Element.getBoundingClientRect() method to get the top offset of your element. This method provides the full offset values (left, top, right, bottom, width, height) of your element in the viewport.

Check the John Resig's post describing how helpful this method is.

tyguy
  • 53
  • 6
Eugene Tiurin
  • 4,019
  • 4
  • 33
  • 32
  • 11
    I would like to point out that this method incurs a significant performance penalty compared to getting just the offset. – vise May 01 '17 at 10:19
  • 3
    @vise can you provide a better answer to this question using just offsetTop or something other that getBoundingClientRect? – TheJKFever May 08 '17 at 22:38
  • 4
    Please when linking to external urls add the content or a summary in case the url becomes invalid in the future. – Dvid Silva Dec 04 '17 at 21:21
  • 1
    This method is imperative, functional maniacs are grinding their teeth. They won't provide you a better solution for performance. – puchu Oct 09 '18 at 14:24
  • 1
    To clarify. you have to use DOM element div, input, etc... it does not work on your React Components (e.g. your own input, kendo, etc). – Jaider Jul 02 '20 at 02:35
74

I do realize that the author asks question in relation to a class-based component, however I think it's worth mentioning that as of React 16.8.0 (February 6, 2019) you can take advantage of hooks in function-based components.

Example code:

import { useRef } from 'react'

function Component() {
  const inputRef = useRef()

  return (
    <input ref={inputRef} />
    <div
      onScroll={() => {
        const { offsetTop } = inputRef.current
        ...
      }}
    >
  )
}
Pa Ye
  • 1,779
  • 12
  • 22
  • 5
    This seems to only give the offsetTop from the closest parent node -- is there an offset from the viewport instead? – Tom Jun 29 '20 at 06:15
  • 1
    This is the best answer! You helped me a lot! const { offsetTop, offsetLeft, offsetHeight } = inputRef.current; console.log('offsettop', offsetTop); console.log('offsetLeft', offsetLeft); console.log('offsetHeight ', offsetHeight); – mr_nobody Jan 29 '21 at 08:06
32

Eugene's answer uses the correct function to get the data, but for posterity I'd like to spell out exactly how to use it in React v0.14+ (according to this answer):

  import ReactDOM from 'react-dom';
  //...
  componentDidMount() {
    var rect = ReactDOM.findDOMNode(this)
      .getBoundingClientRect()
  }

Is working for me perfectly, and I'm using the data to scroll to the top of the new component that just mounted.

sonarforte
  • 1,588
  • 15
  • 18
16

A quicker way if you are using React 16.3 and above is by creating a ref in the constructor, then attaching it to the component you wish to use with as shown below.

...
constructor(props){
   ...
   //create a ref
   this.someRefName = React.createRef();

}

onScroll(){
let offsetTop = this.someRefName.current.offsetTop;

}

render(){
...
<Component ref={this.someRefName} />

}

Chikwado
  • 451
  • 6
  • 7
11

A better solution with ref to avoid findDOMNode that is discouraged.

...
onScroll() {
    let offsetTop  = this.instance.getBoundingClientRect().top;
}
...
render() {
...
<Component ref={(el) => this.instance = el } />
...
Pietro
  • 1,815
  • 2
  • 29
  • 63
11
  import ReactDOM from 'react-dom';
  //...
  componentDidMount() {
    var n = ReactDOM.findDOMNode(this);
    console.log(n.offsetTop);
  }

You can just grab the offsetTop from the Node.

rhigdon
  • 1,483
  • 1
  • 15
  • 20
1

onScroll has a events whict contains all the native and child elements inside this div so u can use it like this shown below and get the targetted element offsetTop.

const getoffSet = e => {
   console.log(e, e.natiiveEvent.target.childNodes[0].offsetTop)
}

return (
   <div onScroll={(e) => getoffSet(e)} ref={listview} style={styles}> 
   </div>
)
Akash Amar
  • 235
  • 4
  • 3
1

Checking if height Property Is Not Set on Parent:

  • If the parent element has no height set then the sticky element won't have any area to stick to when scrolling. This happens because the
    sticky element is meant to stick/scroll within the height of a
    container.

Checking if a Parent Element Is a Flexbox

  • If sticky element's parent is a flexbox, there are two scenarios to check for:

    The sticky element has align-self: auto set (which is the default); The sticky element has align-self: stretch set. If the Sticky Element Has align-self: auto Set: In this case the value of align-self would compute to the parent's align-items value. So,

if the parent has align-items: normal (which is the default) or align-items: stretch set, then it means the height of the sticky element would stretch to fill the entire available space. This would leave no room for the sticky element to scroll within the parent.

If the Sticky Element Has align-self: stretch Set:

In this case, the sticky element would stretch to the height of the parent, and would not have any area to scroll within.

How to Make Sticky Element Scrollable Within a Flexbox: You could simply set the value of the align-self property to align-self: flex-start. This would put the sticky element at the start and won't stretch it.enter link description here

  • If you have a new question, please ask it by clicking the [Ask Question](https://stackoverflow.com/questions/ask) button. Include a link to this question if it helps provide context. - [From Review](/review/late-answers/31794991) – Serhat MERCAN May 24 '22 at 09:44