React onScroll not working - reactjs

I'm trying to add onscroll event handler to specific dom element. Look at this code:
class ScrollingApp extends React.Component {
...
_handleScroll(ev) {
console.log("Scrolling!");
}
componentDidMount() {
this.refs.list.addEventListener('scroll', this._handleScroll);
}
componentWillUnmount() {
this.refs.list.removeEventListener('scroll', this._handleScroll);
}
render() {
return (
<div ref="list">
{
this.props.items.map( (item) => {
return (<Item item={item} />);
})
}
</div>
);
}
}
Code is quite simple, and as you can see, I want to handle div#list scrolling. When I was running this example, it isn't working. So I tried bind this._handleScroll on render method directly, it doesn't work either.
<div ref="list" onScroll={this._handleScroll.bind(this)> ... </div>
So i opened chrome inspector, adding onscroll event directly with:
document.getElementById('#list').addEventListener('scroll', ...);
and it is working! I don't know why this happens. Is this a bug of React or something? or did I missed something? Any device will very appreciated.

The root of the problem is that this.refs.list is a React component, not a DOM node. To get the DOM element, which has the addEventListener() method, you need to call ReactDOM.findDOMNode():
class ScrollingApp extends React.Component {
_handleScroll(ev) {
console.log("Scrolling!");
}
componentDidMount() {
const list = ReactDOM.findDOMNode(this.refs.list)
list.addEventListener('scroll', this._handleScroll);
}
componentWillUnmount() {
const list = ReactDOM.findDOMNode(this.refs.list)
list.removeEventListener('scroll', this._handleScroll);
}
/* .... */
}

Related

How to get the DOM node from a Class Component ref with the React.createRef() API

I have these two components:
import { findDOMNode } from 'react-dom';
class Items extends Component {
constructor(props) {
super(props);
this.ref = React.createRef();
this.selectedItemRef = React.createRef();
}
componentDidMount() {
if (this.props.selectedItem) {
this.scrollToItem();
}
}
componentWillReceiveProps(nextProps) {
if (this.props.selectedItem !== nextProps.selectedItem) {
this.scrollToItem();
}
}
scrollToItem() {
const itemsRef = this.ref.current;
const itemRef = findDOMNode(this.selectedItemRef.current);
// Do scroll stuff here
}
render() {
return (
<div ref={this.ref}>
{this.props.items.map((item, index) => {
const itemProps = {
onClick: () => this.props.setSelectedItem(item.id)
};
if (item.id === this.props.selectedItem) {
itemProps.ref = this.selectedItemRef;
}
return <Item {...itemProps} />;
})}
</div>
);
}
}
Items.propTypes = {
items: PropTypes.array,
selectedItem: PropTypes.number,
setSelectedItem: PropTypes.func
};
and
class Item extends Component {
render() {
return (
<div onClick={() => this.props.onClick()}>item</div>
);
}
}
Item.propTypes = {
onClick: PropTypes.func
};
What is the proper way to get the DOM node of this.selectedItemRef in Items::scrollToItem()?
The React docs discourage the use of findDOMNode(), but is there any other way? Should I create the ref in Item instead? If so, how do I access the ref in Items::componentDidMount()?
Thanks
I think what you want is current e.g. this.selectedItemRef.current
It's documented on an example on this page:
https://reactjs.org/docs/refs-and-the-dom.html
And just to be safe I also tried it out on a js fiddle and it works as expected! https://jsfiddle.net/n5u2wwjg/195724/
If you want to get the DOM node for a React Component I think the preferred way of dealing with this is to get the child component to do the heavy lifting. So if you want to call focus on an input inside a component, for example, you’d get the component to set up the ref and call the method on the component, eg
this.myComponentRef.focusInput()
and then the componentRef would have a method called focusInput that then calls focus on the input.
If you don't want to do this then you can hack around using findDOMNode and I suppose that's why it's discouraged!
(Edited because I realized after answering you already knew about current and wanted to know about react components. Super sorry about that!)

How to best setup a click event when I have 3 different functions as possibilities

I have a component that I will use in my application, and the actual onClick event can have 3 possibilites:
1. function#1
2. function#2
3. no click event
So based on a condition, how can I wireup the correct event on the component?
class UserComponent extends React.Component {
_handleClick() {
console.log('clicked user component');
}
render() {
return(
<div className="user-component" onClick={::this._handleClick}> ... </div>
);
}
}
Would I just use a switch statement?
Or create a function that would then optionally fire one of the 3?
you should use the handle function for your click event to be a routing function. determine what it needs to do from some context and then do that thing
class UserComponent extends React.Component {
possibility1() {
console.log('possibility one called')
}
possibility2() {
console.log('possibility two called')
}
_handleClick() {
if (!this.props.someImportantThing) {
console.log('possibility three, no event called')
return
}
if (this.props.someOtherThing) {
this.possibility1()
} else {
this.possibility2()
}
}
render() {
return(
<div className="user-component" onClick={::this._handleClick}> ... </div>
);
}
}
Edit
if this component is only firing one of those functions, but it needs to be able to fire of a conditional function you can use it like a prop.
class UserComponent extends React.Component {
_handleClick(e) {
this.props.handleClick && this.props.handleClick(e)
}
render() {
return(
<div className="user-component" onClick={::this._handleClick}> ... </div>
);
}
}
then you would use it like this
<UserComponent handleClick={possibility1} /> // I run possibility1 function only
<UserComponent handleClick={possibility2} /> // I run possibility2 function only
<UserComponent /> // I dont run any handler function at all
Inside of _handleClick id fire the correct function based on the event. This would be does fairly easily if the event is passed as a prop or is held in some app wide state like redux.

Correct way to get refs and attach event listeners in React ComponentDidMount?

The problem is roughly summarized in the comments in the code snippet. When I bind this._setSize in constructor, it never knows about this.container — even when called in componentDidMount. What am I doing wrong? Thanks!
export default class MyComponent extends React.Component {
constructor () {
super()
this._setSize = this._setSize.bind(this)
}
componentDidMount () {
const container = this.container // <div></div> :)
this._setSize()
window.addEventListener('resize', this._setSize)
}
componentWillUnmount () {
window.addEventListener('resize', this._setSize)
}
_setSize () {
const container = this.container // undefined :(
const containerSize = {
x: container.offsetWidth,
y: container.offsetHeight
}
this.setState({ containerSize })
}
render () {
return (
<div ref={node => this.container = node}>
</div>
)
}
}
Within each re-render you are creating and passing new instance of function to setup container ref. The previous function is then called with null. Therefore it might happen that you accidently set this.container to null:
<div ref={node => this.container = node}>
When you pass here component instance method instead of inline function, it is called once with reference and second time with null during component unmount. E.g.:
// dont forget to bind it in constructor
onContainerRef (node) {
// furthermore you can even check if node is not null
// if (node) { ...
this.container = node
}
// ... in render
<div ref={this.onContainerRef}>
You can read more in docs.
I fixed your code and it's working now: see working DEMO
What was the problem?
componentWillUnmount () {
window.addEventListener('resize', this._setSize)
}
You didn't remove event listener from window because in componentWillUnmount you have addEventListener instead of removeEventListener. If you have any conditional rendering of the component, on resize event _setSize will be also called.
To illustrate this problem, play with the broken demo and click on Toggle button and look at output: see broken DEMO

React — catch wheel event on overflow:hidden element?

How can I catch a scroll / wheel event in a React component if the element has overflow: hidden?
Something like this:
<Component onWheel={this.handleWheel.bind(this)}/>
So I found your question since I was running on a similar issue. My problem was that my div was not scrollable and I needed to capture the scroll event or the wheel move really. This what I did:
export default class MyComponent extends Component {
constructor(){
super();
}
handleScroll(e) {
console.log('Scroll event is being called', e);
}
componentDidMount() {
const holder = ReactDOM.findDOMNode(this.refs.holder)
holder.addEventListener('mousewheel', this.handleScroll);
}
componentWillUnmount() {
const holder = ReactDOM.findDOMNode(this.refs.holder)
holder.removeEventListener('mousewheel', this.handleScroll);
}
render() {
const hiddenRecipients = amountOfRecipients - children.length;
return (
<div className="my_holder" ref="holder" />)
}
}
So basically I just added a ref and attached an event listener to the mouse wheel.
Hope it helps
const onWheel = e => {
e.preventDefault()
console.log(e.deltaY)
}
<Component onWheel={onWheel} />
It's just another way to handle (catch) wheel data in react

ReactJS - Add custom event listener to component

In plain old HTML I have the DIV
<div class="movie" id="my_movie">
and the following javascript code
var myMovie = document.getElementById('my_movie');
myMovie.addEventListener('nv-enter', function (event) {
console.log('change scope');
});
Now I have a React Component, inside this component, in the render method, I am returning my div. How can I add an event listener for my custom event? (I am using this library for TV apps - navigation )
import React, { Component } from 'react';
class MovieItem extends Component {
render() {
if(this.props.index === 0) {
return (
<div aria-nv-el aria-nv-el-current className="menu_item nv-default">
<div className="indicator selected"></div>
<div className="category">
<span className="title">{this.props.movieItem.caption.toUpperCase()}</span>
</div>
</div>
);
}
else {
return (
<div aria-nv-el className="menu_item nv-default">
<div className="indicator selected"></div>
<div className="category">
<span className="title">{this.props.movieItem.caption.toUpperCase()}</span>
</div>
</div>
);
}
}
}
export default MovieItem;
Update #1:
I applied all the ideas provided in the answers. I set the navigation library to debug mode and I am able to navigate on my menu items only based on the keyboard (as you can see in the screenshot I was able to navigate to Movies 4) but when I focus an item in the menu or press enter, I dont see anything in the console.
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class MenuItem extends Component {
constructor(props) {
super(props);
// Pre-bind your event handler, or define it as a fat arrow in ES7/TS
this.handleNVFocus = this.handleNVFocus.bind(this);
this.handleNVEnter = this.handleNVEnter.bind(this);
this.handleNVRight = this.handleNVRight.bind(this);
}
handleNVFocus = event => {
console.log('Focused: ' + this.props.menuItem.caption.toUpperCase());
}
handleNVEnter = event => {
console.log('Enter: ' + this.props.menuItem.caption.toUpperCase());
}
handleNVRight = event => {
console.log('Right: ' + this.props.menuItem.caption.toUpperCase());
}
componentDidMount() {
ReactDOM.findDOMNode(this).addEventListener('nv-focus', this.handleNVFocus);
ReactDOM.findDOMNode(this).addEventListener('nv-enter', this.handleNVEnter);
ReactDOM.findDOMNode(this).addEventListener('nv-right', this.handleNVEnter);
//this.refs.nv.addEventListener('nv-focus', this.handleNVFocus);
//this.refs.nv.addEventListener('nv-enter', this.handleNVEnter);
//this.refs.nv.addEventListener('nv-right', this.handleNVEnter);
}
componentWillUnmount() {
ReactDOM.findDOMNode(this).removeEventListener('nv-focus', this.handleNVFocus);
ReactDOM.findDOMNode(this).removeEventListener('nv-enter', this.handleNVEnter);
ReactDOM.findDOMNode(this).removeEventListener('nv-right', this.handleNVRight);
//this.refs.nv.removeEventListener('nv-focus', this.handleNVFocus);
//this.refs.nv.removeEventListener('nv-enter', this.handleNVEnter);
//this.refs.nv.removeEventListener('nv-right', this.handleNVEnter);
}
render() {
var attrs = this.props.index === 0 ? {"aria-nv-el-current": true} : {};
return (
<div ref="nv" aria-nv-el {...attrs} className="menu_item nv-default">
<div className="indicator selected"></div>
<div className="category">
<span className="title">{this.props.menuItem.caption.toUpperCase()}</span>
</div>
</div>
)
}
}
export default MenuItem;
I left some lines commented because in both cases I am not able to get the console lines to be logged.
Update #2: This navigation library does not work well with React with its original Html Tags, so I had to set the options and rename the tags to use aria-* so it would not impact React.
navigation.setOption('prefix','aria-nv-el');
navigation.setOption('attrScope','aria-nv-scope');
navigation.setOption('attrScopeFOV','aria-nv-scope-fov');
navigation.setOption('attrScopeCurrent','aria-nv-scope-current');
navigation.setOption('attrElement','aria-nv-el');
navigation.setOption('attrElementFOV','aria-nv-el-fov');
navigation.setOption('attrElementCurrent','aria-nv-el-current');
If you need to handle DOM events not already provided by React you have to add DOM listeners after the component is mounted:
Update: Between React 13, 14, and 15 changes were made to the API that affect my answer. Below is the latest way using React 15 and ES7. See answer history for older versions.
class MovieItem extends React.Component {
componentDidMount() {
// When the component is mounted, add your DOM listener to the "nv" elem.
// (The "nv" elem is assigned in the render function.)
this.nv.addEventListener("nv-enter", this.handleNvEnter);
}
componentWillUnmount() {
// Make sure to remove the DOM listener when the component is unmounted.
this.nv.removeEventListener("nv-enter", this.handleNvEnter);
}
// Use a class arrow function (ES7) for the handler. In ES6 you could bind()
// a handler in the constructor.
handleNvEnter = (event) => {
console.log("Nv Enter:", event);
}
render() {
// Here we render a single <div> and toggle the "aria-nv-el-current" attribute
// using the attribute spread operator. This way only a single <div>
// is ever mounted and we don't have to worry about adding/removing
// a DOM listener every time the current index changes. The attrs
// are "spread" onto the <div> in the render function: {...attrs}
const attrs = this.props.index === 0 ? {"aria-nv-el-current": true} : {};
// Finally, render the div using a "ref" callback which assigns the mounted
// elem to a class property "nv" used to add the DOM listener to.
return (
<div ref={elem => this.nv = elem} aria-nv-el {...attrs} className="menu_item nv-default">
...
</div>
);
}
}
Example on Codepen.io
You could use componentDidMount and componentWillUnmount methods:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class MovieItem extends Component
{
_handleNVEvent = event => {
...
};
componentDidMount() {
ReactDOM.findDOMNode(this).addEventListener('nv-event', this._handleNVEvent);
}
componentWillUnmount() {
ReactDOM.findDOMNode(this).removeEventListener('nv-event', this._handleNVEvent);
}
[...]
}
export default MovieItem;
First off, custom events don't play well with React components natively. So you cant just say <div onMyCustomEvent={something}> in the render function, and have to think around the problem.
Secondly, after taking a peek at the documentation for the library you're using, the event is actually fired on document.body, so even if it did work, your event handler would never trigger.
Instead, inside componentDidMount somewhere in your application, you can listen to nv-enter by adding
document.body.addEventListener('nv-enter', function (event) {
// logic
});
Then, inside the callback function, hit a function that changes the state of the component, or whatever you want to do.
I recommend using React.createRef() and ref=this.elementRef to get the DOM element reference instead of ReactDOM.findDOMNode(this). This way you can get the reference to the DOM element as an instance variable.
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class MenuItem extends Component {
constructor(props) {
super(props);
this.elementRef = React.createRef();
}
handleNVFocus = event => {
console.log('Focused: ' + this.props.menuItem.caption.toUpperCase());
}
componentDidMount() {
this.elementRef.addEventListener('nv-focus', this.handleNVFocus);
}
componentWillUnmount() {
this.elementRef.removeEventListener('nv-focus', this.handleNVFocus);
}
render() {
return (
<element ref={this.elementRef} />
)
}
}
export default MenuItem;
Here is a dannyjolie more detailed answer without need of component reference but using document.body reference.
First somewhere in your app, there is a component method that will create a new custom event and send it.
For example, your customer switch lang.
In this case, you can attach to the document body a new event :
setLang(newLang) {
// lang business logic here
// then throw a new custom event attached to the body :
document.body.dispatchEvent(new CustomEvent("my-set-lang", {detail: { newLang }}));
}
Once that done, you have another component that will need to listen to the lang switch event. For example, your customer is on a given product, and you will refresh the product having new lang as argument.
First add/remove event listener for your target component :
componentDidMount() {
document.body.addEventListener('my-set-lang', this.handleLangChange.bind(this));
}
componentWillUnmount() {
document.body.removeEventListener('my-set-lang', this.handleLangChange.bind(this));
}
then define your component my-set-langw handler
handleLangChange(event) {
console.log("lang has changed to", event.detail.newLang);
// your business logic here .. this.setState({...});
}

Resources