React uses event delegation, as mentioned in the documentation here:
Event delegation: React doesn't actually attach event handlers to the nodes themselves. When React starts up, it starts listening for all events at the top level using a single event listener. When a component is mounted or unmounted, the event handlers are simply added or removed from an internal mapping.
I have a very common scenario where I have a list of items and I want a event handler on each item, should I use my custom event delegation and access target element from event object to perform logic or should I attach individual event listener callbacks to each list item and rely on React to take care of performance.
Attach event handler to each. You might look into paging the list, most displays won't show 500-1000 items at a time.
class SnipListItemRender extends React.Component {
render() {
let SnipSpanSty = {width: 'calc(70% - 142px)'};
SnipSpanSty.color = (this.props.index === this.props.selectedKey) ? '#b58900' : '#afac87';
return (
<div id='SnipDivSty' onclick={this.snipClickHandler} className="FlexBox" style={SnipDivSty}>
<div id='SelectSnipDivSty' style={SelectSnipDivSty}>
<JButton btn={selectSnipBtn} parentClickHandler={this.snipClickHandler} />
</div>
<span id='SnipSpanSty' style={SnipSpanSty}>{this.props.snippet.snip}</span>
<JButton btn={SnipBtn} parentClickHandler={this.snipClickHandler} />
</div>
);
}
}
class SnipListItem extends SnipListItemRender {
snipClickHandler = (buttonid) => { Actions.selectSnipItem(this.props.snippet, buttonid); }
}
let _snipDataMap = function(snip, index) {
return (
<li id='SnipsDivLiSty' key={index} style={SnipsDivLiSty}>
<SnipListItem snippet={snip} index={index} selectedKey={this.props.selectedKey} />
</li>
);
}
export default class SnipsView extends React.Component {
render() {
let list = this.props.data.map(_snipDataMap, this)
return (
<div id='SnipsDivSty' style={SnipsDivSty}>
<ul id='SnipsDivUlSty' style={SnipsDivUlSty}>
{list}
</ul>
</div>
);
}
}
If your custom event delegation means having custom data in event,
you shouldn't do it. Because it against React's philosophy.
New data flow is introduced
React prefer one-way data flow
Implicit dependency is introduced
React prefer passing data and functions to props. All dependencies should be explicit
For more details, you can read this
Related
I am using a component that I cannot change directly, but I would like to extend.
import { Button } from '#external-library'
// Currently how the button component is being used
<Button click={() => doSomething()} />
// I would like to add a tabIndex to the button
<Button click={() => doSomething()} tabIndex={0} />
I cannot add an attribute because the component is not expecting a tabIndex. I cannot directly modify the Button component.
How can I extend the <Button /> component so I can add attributes like tabIndex, etc?
I was hoping something like the following would work:
export default class ExtendedButton extends Button { }
// except I'm dealing with functional components
You can't edit custom component implementation without changing its internals.
// You can't add tabIndex to internal button without changing its implementation
const Button = () => <button>Click</button>;
In such cases, you implement a wrapper with desired props:
const Component = () => {
return (
<div tabIndex={0}>
<Button />
</div>
);
};
If the component forwarding ref (also depends to which element it forwarded in the implementation), you can use its attributes:
// Assumption that Button component forwards ref
const Button = React.forwardRef((props,ref) => <button ref={ref}>Click</button>);
<Button ref={myRef}/>
// Usage
myRef.current.tabIndex = 0;
You can access the inner DOM button element using React refs(read here)
most likely the external-lib you use provide a ref prop for the Button component which you use to pass your own create ref
const buttonRef = useRef(null);
<Button ref={buttonRef}/>
Then you can use buttonRef.current to add tabIndex when your data is ready to be populated in like
useEffect( () => {
if(buttonRef && buttonRef.current){
buttonRef.current.tabIndex = 2;
}
}, [props.someProperty] );
I am using Preact with hooks. I have following button component:
export function Button(props) {
return (
<button class={props.class}>{props.children}</button>
);
}
I have another parent component where I need to access actual DOM element button for animation purpose.
export function Parent(props) {
const buttonElm = useRef(null);
useEffect(() => {
console.log(buttonElm.current);
// Animate button using popmotion or similar
});
return (
<div>
<Button ref={buttonElm}>Click me to animate</Button>
</div>
);
}
However, there is a problem. The buttonElm.current points to JSX object i.e. Button but not the DOM element button. I need buttonElm to point to actual DOM element. How do I do that?
Should I go ahead and use buttonElm.current.base property? But that does not feel idiomatic with hooks.
Also, I have two questions.
How does ref behave when I am setting it on a Preact component that returns multiple elements using <Fragment />.
Second, is accessing the children's DOM element for animation purpose acceptable/correct practice in Preact/React? (I can wrap my component in another wrapper div but that causes more animation headaches than solving the problem)
You need to pass ref as props to your child component. By doing this buttonElm will point to actual Button DOM element.
export function Button(props) {
return (
<button class={props.class} ref={props.buttonElm}>{props.children}</button>
);
}
export function Parent(props) {
const buttonElm = useRef(null);
useEffect(() => {
console.log(buttonElm.current);
// Animate button using popmotion or similar
});
return (
<div>
<Button buttonElm={buttonElm}>Click me to animate</Button>
</div>
);
}
The buttons i create using below seems to lag in the selectedButtonIdx value.
Is the toggleSelected not complete by the time getClass is called ?
function ButtonGroup(props) {
const [selectedButtonIdx,setIdx]=useState(props.loadCurrentAsIndex);
const toggleSelected = (e) => {
setIdx(parseInt(e.target.dataset.index));
props.onclick(e);
};
const getClass = (index) => {
return (selectedButtonIdx === index) ? classnames('current', props.btnClass)
: classnames(props.btnClass)
};
let buttons = props.buttons.map((b, idx) => <Button key={idx} value={b.value} index={idx} text={b.text}
onclick={e => toggleSelected(e)}
btnClass={getClass(idx)}/>);
return (
<div>
{buttons}
</div>
);
}
Every onclick is expected to show the user which button in the group was clicked by changing its class.
By looking at this,
<Button
key={idx}
value={b.value}
index={idx}
text={b.text}
onclick={e => toggleSelected(e)}
btnClass={getClass(idx)}
/>
Button is your custom component,
Two things to notice here,
You have provided onclick (c is small) props, in you actual component it should be onClick={props.onclick}
You have used e.target.dataset.index, to work with dataset we should have attribute with data- prefix. So your index should be data-index in your actual component.
So finally your Button component should be,
const Button = (props) => {
return <button text={props.text} data-index={props.index} onClick={props.onclick} className={props.btnClass}>{props.value}</button>
}
Demo
The function setIdx, returned from useState is asynchronous, this means that it may be not be finished by the time you run your next function (as you guessed).
Take a look at useEffect it allows you to specify a function to run once an item in your state changes, this method will ensure your functions are called in the right order.
By now I don't see anything wrong here.
How it works:
initial render happens, onClick event listener is bound
user clicks a button, event handler calls setIdx triggering new render
new render is initiated, brand new selectedButtonIdx is used for rendering(and for getClass call as well)
See, there is no reason to worry about if setIdx is sync function or async.
I am trying to create a custom event in one component and add an event listener in another component. The component that is listening for the event contains a function that I want to execute on the event. Below are what I have in the two components, I just feel like I'm going about this in the wrong way...
Component #1
toggleWidget() {
const event = new CustomEvent('sliderClicked', {
bubbles: true,
});
const sliderToggle = document.getElementById('input');
sliderToggle.dispatchEvent(event);
this.setState({
checked: !this.state.checked,
});
}
/* and then in my render... */
render() {
const displaySlider = this.state.isSliderDisplayed ? (
<div className="slider-container" >
<label className="switch" htmlFor="input">
<input type="checkbox" checked={this.state.checked} onChange={this.toggleWidget} id="input" />
<span className="slider round" />
</label>
<p className="batch-slider-title"> Batch Widget </p>
</div>) : null;`
Component Two
window.addEventListener('sliderClicked', this.refreshLayout);`
Any ideas as to what I may be doing wrong?
Basically it should work, but in react - if you rendered an element in a component you can use the ref to access it:
<input
type="checkbox"
checked={this.state.checked}
onChange={this.toggleWidget}
id="input"
ref={(c) => this.input = c}
/>
And your toggleWidget function should be something like this:
toggleWidget() {
...
this.input.dispatchEvent(event);
...
}
In React it's pretty common to pass down callbacks from parent to child.
const Child = ({handleClick}) => (
<div onClick={ handleClick } >Click me!</div>
);
const Parent = () => {
const childClickHandler = event => {
// do stuff
alert('My child is calling?');
}
return (
<Child handleClick={ childClickHandler }/>
);
};
Maybe that could work for you? You can try the code here. (JSFiddle)
Refs are generally considered something to avoid in React as they couple components together. see the documentation here:
https://facebook.github.io/react/docs/refs-and-the-dom.html
Your first inclination may be to use refs to "make things happen" in your app. If this is the case, take a moment and think more critically about where state should be owned in the component hierarchy. Often, it becomes clear that the proper place to "own" that state is at a higher level in the hierarchy. See the Lifting State Up guide for examples of this.
Try using a global state container like redux and when you "toggleWidget" in one component, set a property in your redux store. Listen to that property by setting it as a prop in your second component(the one that you want to respond to a change/toggle). On change of that property your component will have the "componentWillReceiveProps" lifecycle method called and you can then have your "responding" component take whatever action you like.
I have a simple list being displayed on the return value of a fetch call. I have some functions firing on selection of an item, but for the life of me, I can't fire the onClick event. I have events bound outside the map that work just fine (the onKeyUp and down), but the onClick while inside the map does not work. Not sure where to go from there.
handleClick() {
// won't fire
console.log('test')
}
render() {
return (
<div className="autocomplete">
<input type="text" placeholder={this.props.fieldName} onKeyDown={this.handleKeyDown} onKeyUp={this.handleKeyUp} onClick={this.handleClick} />
<div className="autocomplete__list" onClick={this.handleClick}>
<ul>
{this.state.list.map((item, index) => <li key={index} className={this.checkActive(index)} onClick={this.handleClick}>{item.firstName}</li>)}
</ul>
</div>
</div>
)
}
No errors thrown, the method does nothing when clicking those fields. If it matters at all, the "list" is absolutely positioned. And here is the constructor
constructor(props) {
super(props)
this.state = {
name: '',
list: [],
cursor: 0
}
this.handleKeyDown = this.handleKeyDown.bind(this)
this.handleKeyUp = this.handleKeyUp.bind(this)
this.checkActive = this.checkActive.bind(this)
this.handleClick = this.handleClick.bind(this)
}
The issue seems to be either:
You have the same handler on a parent element that wraps all of your li's.. Usually the click event would bubble upwards from the children to the parent so this is unlikely.
Your li's are a lower z-index than the parent element. Which means that the li elements lay under an element. so a click event only happens on the element on the top.
Try adding this to your css
.autocomplete__list li {
z-index: 100;
}