Element-less component with bound events - reactjs

Is there a Reacty way to create a wrapper component of another element that has bound DOM events, without producing another element (as in <div>)?
E.g.
class TripleClickWrapper extends Component {
render() {
return <div onClick={::this._onClick}>{this.props.children}</div>
}
_onClick() { /* counts clicks and handles timeouts etc */ }
}
// somewhere else:
<TripleClickWrapper onTripleClick={::this._doSomething}>
<SomeComponent />
</TripleClickWrapper>
I don't want the extra <div> TripleClickWrapper creates, but I want to bind onClick to the wrapper, without passing it down to <SomeComponent>. Any nice way without getting to DOMy (findDOMNode+addEventLisetener+remove on unmout)?
If I didn't need to bind DOM events, I could just return React.Children.only(this.props.children).

You can return an augmented/cloned version of the child component in your render function: https://facebook.github.io/react/docs/top-level-api.html#react.cloneelement
You can add additional props during the cloning process.
render() {
const newProps = /* any props/event handlers you want to add */;
return React.cloneElement(React.Children.only(this.props.children), newProps);
}

Based on the source code (specifically, this line: https://github.com/facebook/react/blob/v15.0.0-rc.2/src/renderers/dom/shared/ReactDOMComponent.js#L621 ) I see that a possible solution might be to inject an EventPlugin. This is because enqueuePutListener is called, which in turn calls ReactBrowserEventEmitter.listenTo --> ReactEventListener.trapBubbledEvent --> EventListener.listen which does the actual addEventListener. The if statement there looks for a plugin name. I'm not sure how that mechanism works, and didn't find it in the docs, but with some investigation sounds like a possible solution. For example, there's a plugin that handled tap events when 300ms delay was still a thing.

I ended up with this approach:
I didn't want an extra <div>
I didn't want my wrapper to override the child's events (so if my wrapped div already had onClick it should still run
I didn't want to use addEventListener manually on the findDOMNode(this). Reasons: 1) inner onClicks can't run e.stopPropagation() and stop my added event, as React events run on the document, without useCapture, and 2) lose React event system goodies.
Currently I'm taking the only child in the wrapper and use cloneElement, as #Calvin Belden recommended, and pass in my events combined with existing events.
render() {
const onlyChild = React.Children.only(children)
const events = {
onClick: this.doSomething,
onMouseEnter: this.doSomethingElse,
// ...
}
for (let [eventName, fn] of Object.entries(events)) {
if (onlyChild.props[eventName]) {
let oldFn = onlyChild.props[eventName]
events[eventName] = e => {
oldFn(e)
if (!e.isPropagationStopped()) fn(e)
}
}
}
return React.cloneElement(onlyChild, events)
}
It still forces me to pass in events when I use a component instead of a div: <TripleClickWrapper><SomeComponent></TripleClickWrapper>. SomeComponent would have to pass down onClick to its div.
Not optimal, but as clean as it can be.

Related

How to handle initialisation and events from external library?

I am importing an external library (from a cdn; I know it's terrible) that needs to be initialised with some kind of config, and produces some kind of events.
Parent:
eventTrigerred = e => console.log(e)
...
<Component
onEvent= {this.eventTrigerred}
/>
Child:
render() {
const initiliseLibrary = () => {
let config = {
key: "XXX", // some key to initilise the library
onLibraryEvent: this.props.onEvent // the event I want to handle
}
window.StrangeLibrary.init(config)
}
return (
<div id="library-area">
{initialiseFrames()} // when initilised this library will render an elemnt here
</div>
);
}
The problem is that if in the parent the state changes, this reloads the child, and this result in the child being re-rendered, the event triggering again, and this infinitely loops.
I feel that I am doing somehting fundamentaly wrong, and I wish I did not have to use this library but I have to.
Any idea what the problem might be?
The render function shouldn't create any side effect. Instead, put your initialisation code in componentDidMount, like so :
componentDidMount() {
let config = {
key: "XXX", // some key to initilise the library
onLibraryEvent: this.props.onEvent // the event I want to handle
}
window.StrangeLibrary.init(config)
}
However, this code will be called again if the component will be remounted (but not if it's rendered again without remount, from a prop change, or a state change).
If it's still looping, you may want to lift the initialisation code in a parent component's componentDidMount that stays mounted all along.

Why does Object.keys(this.refs) not return all keys?

Hi,
so I've redacted some sensitive information from the screen shot, but you can see enough to see my problem.
Now, I'm trying to build the UI for a site that gets data from a weather station.
I'm trying to use react-google-maps' InfoBox, which disables mouse events by default.
It seems that to enable mouse events, you must wait until the DOM is loaded, and then add the event handlers.
react-google-maps' InfoBox fires an onDomReady event (perhaps even upon adding more divs) but seems to never fire an onContentChanged event (I've looked in the node_modules code).
The content I'm putting in the InfoBox is basically a div with a string ref for each type of weather data. Sometimes there comes along a new type of weather data so I want to put that in also, and have the ref be available / usable.
However, immediately after the new divs have been added (and the DOM has been updated to show them), when I try to console log the DOM nodes (the refs refer to the nodes because they are divs and not a custom built component) the latest added ones are undefined.
They do become a div (not undefined) a few renders later.
I've contemplated that this may be because
1) the DOM is not being updated before I'm trying to access the refs, but indeed the UI shows the new divs,
2) string refs are deprecated (React 16.5),
but they work for the divs in comonentDidMount and eventually for new divs in componentDidUpdate,
3) executing the code within the return value of render may be run asynchronously with componentDidMount, but I also tried setTimeout with 3000 ms to the same effect,
4) of something to do with enumerable properties, but getOwnProperties behaves the same way.
In the end I decided I'll console log this.refs and Object.keys(this.refs) within the same few lines of code (shown in the screen shot), and you can see that within one console log statement (where Object.keys was used in the previous line) that while this.refs is an object with 8 keys, the two most recently added refs don't appear in Object.keys(this.refs).
This is probably a super complex interaction between react-google-maps' InfoBox, React's refs, and JavaScript's Object.keys, but it seems like it should be simple and confuses me to a loss.
Can anyone shed some light on why this might be happening??
The code looks something alike:
class SensorInfoWindow extends React.Component {
handleIconClick = () => {
// do stuff here
}
componentDidMount() {
this.addClickHandlers();
}
componentDidUpdate() {
this.addClickHandlers();
}
addClickHandlers = () => {
const keys = Object.keys(this.refs);
for(let i=0; i<keys.length; i++) {
const key = keys[i];
let element = this.refs[key];
if (element !== undefined)
element.addEventListener('click', this.handleIconClick);
}
}
render() {
const { thissensor, allsensors } = this.props;
let divsToAddHandlersTo = [];
const sensorkeys = Object.keys(allsensors);
for (let i=0; i<sensorkeys.length; i++) {
divsToAddHandlersTo.push(
<div
ref={'stringref' + i}
/>
{/* children here, using InfoBox */}
</div>
);
}
return (
<div>
{divsToAddHandlersTo}
</div>
);
}
}
This is, in essence, the component.

When testing a React component with Enzyme is it better to use simulate or to call the method directly on the instance()?

If you have a component like this
class Editor extends Component {
handleChange() {
// some code
}
render() {
<div>
<input className="Editor" onChange={this.handleChange} />
</div>
}
}
Is it better to test the handle change by simulating the change event with simulate like this:
wrapper.simulate('change', { // })
Or by calling the method directly by using instance:
wrapper.instance().handleChange()
If you are using Shallow Rendering then .simulate just tries to find the right prop and call it. From the Common Gotchas section for .simulate:
Even though the name would imply this simulates an actual event, .simulate() will in fact target the component's prop based on the event you give it. For example, .simulate('click') will actually get the onClick prop and call it.
There isn't any advantage to calling .simulate when using Shallow Rendering and simply calling the prop directly avoids issues caused by the event not mapping to the correct prop.
If you are using Full DOM Rendering then .simulate will fire an event that ends up calling runEventsInBatch from the comically named ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events.
So using .simulate with mount will actually simulate the event, just note from the Common Gotchas section that:
ReactWrapper will pass a SyntheticEvent object to the event handler in your code. Keep in mind that if the code you are testing uses properties that are not included in the SyntheticEvent, for instance event.target.value, you will need to provide a mock event...for it to work.
For Full DOM Rendering it is up to you to determine if there is any value in having ReactDOM simulate the event to call your handler or to just call your handler directly.
From this post by an Airbnb dev:
In general, I've found it's best to invoke the prop directly and avoid .simulate.
For your test it would look something like this:
test('onChange', () => {
const wrapper = shallow(<Editor />); // works with shallow or mount
const onChangeHandler = wrapper.find('input').prop('onChange');
// test onChangeHandler
})

Are there any advantages of React's synthetic events vs. addEventListener?

I have a React component that manages an HTML audio element. AFAIU there are two ways this can be accomplished: Put the audio element as a property on the class instance with, or put it in the render() method and stick a ref on it.
Because the second option does not create the audio element directly, but via React.createElement, I can use Reacts synthetic event system - while the first option requires adding event listeners with addEventListener. My question is, if there are any advantages of the second option?
(Option 1)
class A extends React.Component {
audio = new Audio();
componentDidMount() {
this.audio.addEventListener('play', () => console.log('Started playing'));
}
play() {
this.audio.play();
}
render() {
return (
<div>
<button onClick={this.play}>Play</button>
</div>
);
}
}
(Option 2)
class A extends React.Component {
play() {
this.audio.play();
}
render() {
return (
<div>
<audio
ref={audio => this.audio = audio}
onPlay={() => console.log('Started playing')}
/>
<button onClick={this.play}>Play</button>
</div>
);
}
}
Synthetic event is just a wrapper for normal events. They are there to provide a common interface between browser inconsistencies and large amounts of events ultimately perform better because of event pooling.
You are access the orginial event through e.nativeEvent.
Synthetic events are better in the sense that they are easier to use (much less code, supported without hassle between all browsers) and they are more reliable since React adds them after the element in rendered and is available in the DOM. When componentDidMount is called you can't be sure if the element is actually present in the browser DOM. componentDidMount is called when React tells the browser to append the element to DOM and has calculated the necessary stuff in it's own virtual DOM. But the browser may not render the element if the render thread is busy in any way.

ReactJS: always transfer props to child component

I have a higher-order component that tracks the mouseover event of any composed component. It is roughly like this:
(Component) => class TrackMouseOver extends React.Component {
handleMouseOver = (e) => {
console.log('Mouse over!');
};
render() {
return <Component {...this.props} onMouseOver={this.handleMouseOver}/>;
}
};
However, this does not work very well since React components do not bind to DOM event themselves. Unless the component handle the onMouseOver prop, the HOC above won't work.
Without breaking the black-box nature of React components, is it better to always transfer props to child element? If yes, will there be significant performance impact by doing so?
If you start passing your handler function down, you will be going against the natural flow of events, as they bubble up through the DOM.
Instead, wrap your component in a handling element with a listener and make use of event bubbling.
(Component) => class TrackMouseOver extends React.Component {
handleMouseOver(e) {
e.target // is the element that triggered the event
e.currentTarget // is the element this handler is attached to
}
render() {
return (
<div onMouseOver={this.handleMouseOver}>
<Component {...this.props} />
</div>
);
}
};
When you click on one of the elements inside the new div, the event has a two part lifecycle.
Capturing
The browser works out which top level element the event happened to, then which child of that element and so on until the event finally ends up at an element with no children — the target.
This sequence won't trigger any event listeners (unless they have the useCapture flag set), it just identifies the path of the event.
Bubbling
Now the event's target element has been reached, the event bubbles back up the captured path to the root of the DOM, triggering event listeners as it goes.
This is the reason that event handlers attached to the body element always trigger*, regardless of which element the event happens to. The body is a common ancestor for all elements.
In your case, you can utilise the fact that events for any elements inside your div will eventually bubble their way up to this event handler.
You can work out which element the event came from using the event.target.
* Event handlers won't trigger if an event handler on one of their descendant elements calls event.preventDefault or event.stopPropagation.

Resources