I created a HOC to listen for clicks outside its wrapped component, so that the wrapped component can listen and react as needed.
The HOC looks like this :
const addOutsideClickListener = (WrappedComponent) => {
class wrapperComponent extends React.Component {
constructor() {
super();
this._handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener('click', this._handleClickOutside, true);
}
componentWillUnmount() {
document.removeEventListener('click', this._handleClickOutside, true);
}
_handleClickOutside(e) {
// 'this' here refers to document ???
const domNode = ReactDOM.findDOMNode(this);
if ((!domNode || !domNode.contains(e.target))) {
this.wrapped.handleClickOutside();
}
}
render() {
return (
<WrappedComponent
ref={(wrapped) => { this.wrapped = wrapped }}
{...this.props}
/>
);
}
}
return wrapperComponent;
}
Whenever I click anywhere, I get the error "Uncaught Error: Element appears to be neither ReactComponent nor DOMNode" on the _handleOutsideClick callback.
Any ideas what could be causing this ?
Update:
OK so the source of the error is that "this" inside _handleClickOutside is now referring to 'document', which is what is expected
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#The_value_of_this_within_the_handler
This looks to be an absolute mess - it seems I can either bind the event correctly but then not be able to unbind it, or I can unbind it correctly but the binding method will throw an error...
Try using this -
constructor() {
super();
this._handleClickOutsideRef = this._handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener('click', this._handleClickOutsideRef, true);
}
componentWillUnmount() {
document.removeEventListener('click', this._handleClickOutsideRef, true);
}
Binding has to be done like this -
constructor() {
super();
this._handleClickOutside = this._handleClickOutside.bind(this);
}
or use arrow function for _handleClickOutside.
_handleClickOutside = (e) => {
// 'this' here refers to document ???
const domNode = ReactDOM.findDOMNode(this);
if ((!domNode || !domNode.contains(e.target))) {
this.wrapped.handleClickOutside();
}
}
Related
I'm trying to implement the strategy design pattern to dynamically change how I handle mouse events in a react component.
My component:
export default class PathfindingVisualizer extends React.Component {
constructor(props) {
super(props)
this.state = {
grid: [],
mouseLeftDown: false,
};
const mouseStrat2 = null; // Object I will change that has different functions for handling events
}
componentDidMount() {
this.resetGrid();
this.mouseStrat2 = new StartEndStrat();
}
render() {
//buttons that change the object i want handling mouse events
<button onClick={() => this.mouseStrat2 = new StartEndStrat(this)}>startendstrat</button>
<button onClick={() => this.mouseStrat2 = new WallStrat(this)}>wallstrat</button>
}
}
I want my mouse strats that will access change the component with differing methods to handle mouse events
export class StartEndStrat {
handleMouseDown(row, col) {
// I want to access component state and call functions of the component
this.setState({ mouseLeftDown: true });
PathfindingVisualizer.resetGrid();
}
//other functions to change other stuff
handleMouseEnter(row, col) {
console.log('start end strat');
}
}
export class WallStrat {
handleMouseDown(row, col) {
this.setState({ mouseLeftDown: true });
}
handleMouseEnter(row, col) {
console.log('wallstrat');
}
}
You can try use Refs to do this.
refOfComponent.setState({ ... })
But I would rather recommend you to avoid such constructions as this may add complexity to your codebase.
Solution I found was to use a ref callback to make the DOM element a global variable.
<MyComponent ref={(MyComponent) => window.MyComponent = MyComponent})/>
Then you can access MyComponent with window.MyComponent, functions with window.MyComponent.method() or state variables with window.MyComponent.state.MyVar
My App.js:
function App() {
return (
<div className="App">
<PathfindingVisualizer ref={(PathfindingVisualizer) => {window.PathfindingVisualizer = PathfindingVisualizer}} />
</div>
);
}
Other.js:
handleMouseDown() {
window.PathfindingVisualizer.setState({mouseLeftDown: true});
}
I'm very new to modules and components and working with frameworks. I'm trying to work with Gia to get started.
I've set up an eventListener which gets added to a button within Gia's mount() method and it binds the component in order to have access to the component's element (change classes, style, etc.).
How do I remove the eventListener in the unmount() method? I know the issue is due to the new function-reference created by the bind() method. I just don't know how to access and remove it.
class navigation extends Component {
//
// Construct
constructor(element) {
super(element);
//
this.ref = {
navButton: null,
navLinks: [],
};
}
//
// Mount
mount() {
//
// Listen for click
var listener = this.handleClick.bind(this);
this.ref.navButton.addEventListener("click", listener);
//
}
//
// Unmount
unmount() {
//
// Stop listening for click
this.ref.navButton.removeEventListener("click", listener);
}
//
// Click-handlers
handleClick() {
this.element.classList.toggle("closed");
}
}
//
// Export component
export default navigation;
(Similar answers provide solutions for React scenarios, I was not able to adapt them for my case, hence this question.)
Instead of defining the listener variable inside mount, define a variable in the constructor and use the same for adding or removing the listener
class navigation extends Component {
//
// Construct
constructor(element) {
super(element);
//
this.ref = {
navButton: null,
navLinks: [],
};
this.listener = this.handleClick.bind(this);
}
// Mount
mount() {
this.ref.navButton.addEventListener("click", this.listener);
}
// Unmount
unmount() {
// Stop listening for click
this.ref.navButton.removeEventListener("click", this.listener);
}
// Click-handlers
handleClick() {
this.element.classList.toggle("closed");
}
}
//
// Export component
export default navigation;
Hope this solves the issue that you are facing.
The same example using arrow functions instead of using bind
class navigation extends Component {
//
// Construct
constructor(element) {
super(element);
//
this.ref = {
navButton: null,
navLinks: [],
};
}
// Mount
mount() {
this.ref.navButton.addEventListener("click", this.handleClick);
}
// Unmount
unmount() {
// Stop listening for click
this.ref.navButton.removeEventListener("click", this.handleClick);
}
// Click-handlers
handleClick = () => {
this.element.classList.toggle("closed");
}
}
//
// Export component
export default navigation;
I got button component which has another child component to show tooltip. I pass ref to <Tooltip> component where I attach event listener to mouseEnter and mouseLeave event to my button.
<Button
ref={this.buttonRef}
type={this.props.type}
className={this.props.className}
disabled={this.props.operationIsActive || (this.props.enabled == undefined ? undefined : !this.props.enabled)}
onClick={this.props.onClick}
onSubmit={this.props.onSubmit}
icon={this.props.icon}
>
{this.props.children}
</Button>
<Tooltip domRef={this.buttonRef} text={this.props.tooltip} />
Here is my Tooltip component
export class Tooltip extends ComponentBase<TooltipProps, TooltipState> {
private documentRef = null;
public componentDidMount() {
super.componentDidMount();
this.documentRef = this.props.domRef
if (this.props.domRef) {
const dom = this.props.domRef.current;
dom.element.addEventListener("mouseenter", this.showTooltip);
dom.element.addEventListener("mouseleave", this.hideTooltip);
}
}
public componentWillUnmount() {
if (this.documentRef) {
const dom = this.documentRef.current;
if (dom) {
dom.element.removeEventListener("mouseenter", this.showTooltip);
dom.element.removeEventListener("mouseleave", this.hideTooltip);
}
}
}
private showTooltip = (): void => {
this.updateState({ isTooltipVisible: true })
}
private hideTooltip = (): void => {
this.updateState({ isTooltipVisible: false })
}
private getStyles(position: any): React.CSSProperties {
const css: React.CSSProperties = {
left: position[0].left,
top: position[0].top + 40,
}
return css;
}
public render() {
if (!this.props.domRef.current) {
return null;
}
if(!this.state.isTooltipVisible)
{
return null;
}
const position = this.props.domRef.current.element.getClientRects();
const css = this.getStyles(position);
return ReactDOM.createPortal(<div style={css} className="tooltip">{this.props.text}</div>, document.getElementById(DomRoot) )
}
}
Everything works fine, but when onClick event is fired on button (for example I got button which only set new state to some component and after that simple div will be rendered), componentWillUnmount method is triggered and ref is lost so I cannot remove those two listeners in Tooltip component. Is it possible to unmount children before parent or any other way how I can fix this?
A ref is just { current: ... } object. The purpose of this recipe is to be able to update current property after ref object was passed by reference.
In the code above <Button> is mounted and unmounted before <Tooltip>. The ability of a ref to secretly update current is misused here. Tooltip already relies on that a ref contains a reference to specific component. It isn't expected that a ref will change during Tooltip lifespan, so it shouldn't rely on transient ref. It should be:
public componentDidMount() {
super.componentDidMount();
this.reference = this.props.domRef.current;
if (this.reference) {
this.reference.element.addEventListener("mouseenter", this.showTooltip);
this.reference.element.addEventListener("mouseleave", this.hideTooltip);
}
}
public componentWillUnmount() {
if (this.reference) {
dom.element.removeEventListener("mouseenter", this.showTooltip);
dom.element.removeEventListener("mouseleave", this.hideTooltip);
}
}
I am using a callback function selectionHandlerList() to set my state in Parent component (AudiobookList) from Child (AudiobookDetail). When I run the below code, I get this error message: "TypeError: this.setState is not a function".
When commenting out this.setState({ selectedAudiobook: someArg }) in my selectionHandlerList() function, I receive the expected value from the Child component (from console.log(someArg), so the callback must be working properly.
This is the code of my Parent component:
class AudiobookList extends Component {
constructor(props) {
super(props);
this.selectionHandlerList.bind(this);
}
state = {
audiobooks: [],
selectedAudiobook: null
}
componentWillMount() {
axios.get('http://www.demo.demo/api/get/all')
.then(response => this.setState({
audiobooks: response.data }))
.catch(e => console.log(e));
}
selectionHandlerList(someArg) {
this.setState({ selectedAudiobook: someArg });
console.log(someArg);
}
renderAudiobookChoice(audioBookChoice, selectionHandlerList) {
if (audioBookChoice === 'all') {
return (
this.state.audiobooks.map(audiobook =>
<AudiobookDetail
key={audiobook.id}
audiobook={audiobook}
selectionHandlerList={selectionHandlerList}
/>)
);
}
if (audioBookChoice === 'prose') {
return this.state.audiobooks.map(audiobook => {
return audiobook.text_type === 1 ?
<AudiobookDetail
key={audiobook.id}
audiobook={audiobook}
selectionHandlerList={selectionHandlerList}
/>
:
null;
});
}
if (audioBookChoice === 'poetry') {
return this.state.audiobooks.map(audiobook => {
return audiobook.text_type === 2 ?
<AudiobookDetail
key={audiobook.id}
audiobook={audiobook}
selectionHandlerList={selectionHandlerList}
/>
:
null;
});
}
}
render() {
console.log('ABL: ' + this.state.selectedAudiobook);
const selectionHandlerList = this.selectionHandlerList;
const { audioBookChoice } = this.props;
return (
<ScrollView>
{this.renderAudiobookChoice(
audioBookChoice,
selectionHandlerList
)}
</ScrollView>
);
}
}
export default AudiobookList;
I am under the impresseion, that the issue must be with binding this properly. But I am not able of figuring it out from related questions.
Any help is much appreciated.
Calling .bind on a function returns a new version of that function. You have called .bind, but you also have to reassign the function to the "bound" version of itself.
"TypeError: this.setState is not a function" is almost always caused by this simple error. Something like this should correct the issue:
constructor(props) {
super(props);
this.selectionHandlerList = this.selectionHandlerList.bind(this);
}
This part of the React docs explains more about why this is the case https://reactjs.org/docs/components-and-props.html#es6-classes
Another way of structuring your code which avoids this problem is to use an arrow function class property. For example:
selectionHandlerList = () => {
// this.[everything] works here, because `this`
//is preserved from outer scope within an arrow function
};
This feature still a proposal, but it does have babel support. You would need to enable transform-class-properties or enable stage-2 in Babel to use this syntax.
I'm trying to add infinite scroll in my web application.When the user scrolls down the page, there must be an API call to load the data beneath the existing data.So, the problem here is when I reach the bottom of the web page, the API is not being called.
import React from "react";
import { Link } from 'react-router-dom';
import axios from 'axios';
class InfiniteData extends React.Component{
constructor(props){
super(props);
this.state={olddata: [],newData: [], requestSent: false}
}
componentDidMount(){
window.addEventListener('scroll', this.handleOnScroll);
this.doQuery();
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleOnScroll);
}
doQuery() {
console.log("indoquery");
axios.get("https://jsonplaceholder.typicode.com/posts")
.then( res=>
this.setState({
olddata: res.data,
newData: this.state.olddata.concat(this.state.olddata)
})
)
.catch(console.log("error"))
}
handleOnScroll(){
var scrollTop = (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
var scrollHeight = (document.documentElement && document.documentElement.scrollHeight) || document.body.scrollHeight;
var clientHeight = document.documentElement.clientHeight || window.innerHeight;
var scrolledToBottom = Math.ceil(scrollTop + clientHeight) >= scrollHeight;
if (scrolledToBottom) {
console.log("At bottom");
// enumerate a slow query
setTimeout(this.doQuery, 2000);
}
}
render()
{
return (
<div>
<div className="data-container">
{this.state.newData && this.state.newData.map((dat,i)=>
<div key={i}>
{dat.body}
</div>
)}
</div>
</div>
);
}
}
export default InfiniteData;
This is actually just an obscured case of not binding this correctly: the following line calls handleOnScroll using window (not the InfiniteData component instance) as this:
window.addEventListener('scroll', this.handleOnScroll);
Then, your setTimeout call is trying to call this.doQuery, which is undefined since window.doQuery doesn't exist.
If you bind this correctly for the EventListener, this should work out: either change to window.addEventListener('scroll', this.handleOnScroll.bind(this)); in componentDidMount or add the following line inside the constructor to keep it bound across the board:
this.handleOnScroll = this.handleOnScroll.bind(this)
Note: this isn't the problem you're having, but be careful inside your setState call--do you mean to use newData: this.state.olddata.concat(res.data)? (passing res.data to the concat call)
Following should work.
import React from "react";
function doQuery() {
console.log("indoquery");
// Do something here
}
class InfiniteData extends React.Component{
constructor(props){
super(props);
this.state={olddata: [],newData: [], requestSent: false}
}
componentDidMount(){
window.addEventListener('scroll', this.handleOnScroll);
doQuery();
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleOnScroll);
}
handleOnScroll(){
// .....
if (scrolledToBottom) {
setTimeout(function () {doQuery()}, 2000);
}
}
render() {
return (
<div>
</div>
);
}
}
export default InfiniteData;
Note that I have removed some code to make it compact and easier for you to understand where the problem lies. Basically the fix is where you define the doQuery function and how you pass that function to setTimeout
As Daniel Thompson stated, the problem is that you do not bind this. To elaborate: when the eventListener invokes the function this will not be bound to your component, but rather to window.
Binding this in the event listener will solve the problem of the code not being called, but will create a new problem. When you call handleOnScroll.bind(this) a new function is created, so when you try to un-register it, it will not unregister. To get both registration and unregistration to work as expected, save that new function and use it to register/unregister.
constructor(props){
super(props);
// Need to bind 'this' to refer to component.
// This will create a new function which will have to be stored so it can be unregistered
this.scrollListener = this.handleOnScroll.bind(this);
this.state={olddata: [],newData: [], requestSent: false}
}
componentDidMount(){
window.addEventListener('scroll', this.scrollListener);
this.doQuery();
}
componentWillUnmount() {
window.removeEventListener('scroll', this.scrollListener);
}