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;
Related
I'm trying to define a global state in a react project using React.useContext & React.useState.
I currently have something like this:
# GlobalState.js
class GlobalState extends AbstractState {
register(current, setCurrent) {
this.current = current
this.setCurrent = setCurrent
}
...
}
const globalState = new GlobalState()
export default globalState
And this is passed from App by something like:
const [current, setCurrent] = React.useState({})
globalState.register_state(current, setCurrent)
const state = {current: current, setCurrent: setCurrent}
return React.createElement(GlobalContext.Provider, {value: state},
...children
)
and it is used at some point as follows:
class WebRequestsTimeChart extends React.Component {
render(){
...
return React.createElement(GlobalContext.Consumer, null, (state) => {
return this.#getChart(state)
})
}
componentDidMount() {
console.log(GlobalState.setCurrent) // actual react dispatch content
}
componentWillUnmount() {
console.log(GlobalState.setCurrent) // actual react dispatch content
}
#getChart(state) {
console.log(GlobalState.setCurrent) // actual react dispatch content
return React.createElement(VictoryChart, {
...
containerComponent: React.createElement(VictoryZoomVoronoiContainer, {
...
events: {
onClick: (event) => {
console.log(state.setCurrent) // actual react dispatch content
console.log(GlobalState.setCurrent) // f(){} <--- ???
}
}
}
}
}
So somehow, lambda passed to onClick cannot access the GlobalState.setCurrent but it has no problem accessing state.setCurrent. If I define another method like GlobalState.someRandomMethod, this is still accessible by onClick. Somehow, setCurrent returned by React.useState seems to be behave differently. I also realized this method cannot be copied (eg. structuredClone doesn't work for that). Another thing I considered is Difference between class object Method declaration React?, but I'm not sure how this would cause GlobalState.setCurrent to behave differently.
adding below also doesn't change this behavior:
constructor(props) {
super(props);
this.getChart = this.getChart.bind(this).bind(GlobalState);
}
Please let me know if you have any idea on what is going on, and how I can use GlobalState.setCurrent inside onClick
GlobalState.setCurrent would be a static method on the class itself, which you haven't declared. It's not the same thing as a setCurrent instance method.
class Foo {
bar () {
return 'bar';
}
}
console.log(Foo.bar?.()); // undefined. "bar" doesn't exist on the class itself.
const foo = new Foo();
console.log(foo.bar()) // 'bar'; exists on instance of Foo.
class Baz {
static boom () {
return "static boom";
}
boom () {
return "instance boom";
}
}
console.log(Baz.boom()); // static boom
const baz = new Baz();
console.log(baz.boom()); // instance boom
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 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);
}
}
In my React application, I would like a button that normally says Save, changes its text to Saving... when clicked and changes it back to Save once saving is complete.
This is my first attempt:
import React from 'react';
import { Button } from 'react-bootstrap';
class SaveButton extends React.Component {
constructor(props) {
super(props);
this.state = { isSaving : false };
this.onClick = this.onClick.bind(this);
}
onClick() {
// DOES NOT WORK
this.setState({ isSaving : true });
this.props.onClick();
this.setState({ isSaving : false });
}
render() {
const { isWorking } = this.state;
return (
<Button bsStyle="primary"
onClick={isWorking ? null : this.onClick}>
{isWorking ? 'Saving...' : 'Save'}
</Button>
);
}
}
export default SaveButton;
This doesn't work because both setState and this.props.onClick() (passed in by the parent component) are executed asynchronously, so the calls return immediately and the change to state is probably lost in React optimization (and would probably be visible only for a few milliseconds anuway...).
I read up on component state and decided to lift up state to where it belongs, the parent component, the form whose changes the button saves (which in my app rather is a distant ancestor than a parent). So I removed isSaving from SaveButton, replaced
const { isWorking } = this.state;
by
const { isWorking } = this.props.isWorking;
and tried to set the state in the form parent component. However, this might be architecturally cleaner, but essentially only moved my problem elsewhere:
The actual saving functionality is done at a totally different location in the application. In order to trigger it, I pass onClick event handlers down the component tree as props; the call chain back up that tree upon a click on the button works. But how do I notify about completion of the action in the opposite direction, i.e. down the tree ?
My question: How do I notify the form component that maintains state that saving is complete ?
The form's parent component (which knows about save being complete) could use a ref, but there isn't only one of those forms, but a whole array.
Do I have to set up a list of refs, one for each form ?
Or would this be a case where forwarding refs is appropriate ?
Thank you very much for your consideration! :-)
This doesn't work because both setState and this.props.onClick()
(passed in by the parent component) are executed asynchronously, so
the calls return immediately and the change to state is probably lost
in React optimization
setState can take a callback to let you know once state has been updated, so you can have:
onClick() {
this.setState({ isSaving : true }, () => {
this.props.onClick();
this.setState({ isSaving : false });
});
}
If this.props.onClick() is also async, turn it into a promise:
onClick() {
this.setState({ isSaving : true }, () => {
this.props.onClick().then(() => {
this.setState({ isSaving : false });
});
});
}
can you see ?
https://developer.mozilla.org/tr/docs/Web/JavaScript/Reference/Global_Objects/Promise
parent component's onClick function
onClick(){
return new Promise(function(resolve,reject){
//example code
for(let i=0;i<100000;i++){
//or api call
}
resolve();
})
}
SaveButton component's onClick funtion
onClick(){
this.setState({ isSaving : true });
this.props.onClick().then(function(){
this.setState({ isSaving : false });
});
}
Here is your code that works :
import React from 'react';
import { Button } from 'react-bootstrap';
class SaveButton extends React.Component {
constructor() {
super();
this.state = {
isSaving: false
};
this.handleOnClick = this.handleOnClick.bind(this);
}
handleOnClick() {
this.setState(prevState => {
return {
isSaving: !prevState.isSaving
}
});
console.log("Clicked!",this.state.isSaving);
this.handleSave();
}
handleSave() {
// Do what ever and if true => set isSaving = false
setTimeout(()=>{
this.setState(prevState=>{
return {
isSaving: !prevState.isSaving
}
})
},5000)
}
render() {
const isWorking = this.state.isSaving;
return (
<Button
bsStyle="primary"
onClick={this.handleOnClick}
>
{isWorking ? 'Saving...' : 'Save'}
</Button>
);
}
}
export default SaveButton;
You need to change const isWorking instead of const {isWorking} and you need to handle save function in some manner.
In this example I used timeout of 5 second to simulate saving.
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();
}
}