How to handle initialisation and events from external library? - reactjs

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.

Related

Using refs to modify an element in React.js

Is it wrong to use refs to modify an element's properties? If so, why?
Example:
myRef.current.innerHTML = "Some content";
That's wrong if it's possible to modify the component's JSX to implement the change instead. Whenever possible, one should be able to determine the JSX that gets rendered solely from the current state of the component; direct DOM mutation side-effects like .innerHTML should only be done when there's no other possible option.
For this case, put the content into a state variable instead, like:
const [spanContents, setSpanContents] = useState('foobar');
const changeSpanContents = () => {
setSpanContents('Some content');
};
return (
<div>
<button onClick={changeSpanContents}>click</button>
<span>{spanContents}</span>
</div>
);
In some unusual cases, there exists no JSX syntax for the DOM mutation you want - for example, for putting a resize listener on the window. In such a case, you will have to resort to using vanilla DOM methods instead of doing it solely through React. The following pattern is common for such a case:
useEffect(() => {
const handler = () => {
// resize detected
};
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
}, []);
I wouldn't say wrong but it depends on what you are going to do after changing the html. As React will loose control over that element, you would have a hard time if you need to work on that DOM. Also, it is risky because React have controls over DOM, when you change it manually, it could lead to unexpected behaviors.

useEffect not triggering when object property in dependence array

I have a context/provider that has a websocket as a state variable. Once the socket is initialized, the onMessage callback is set. The callback is something as follows:
const wsOnMessage = (message: any) => {
const data = JSON.parse(message.data);
setProgress(merge(progress, data.progress));
};
Then in the component I have something like this:
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress[pvc.metadata.uid]])
return (
{/* stuff */}
);
}
However, the effect isn't triggering when the progress variable gets updated.
The data structure of the progress variable is something like
{
"uid-here": 0.25,
"another-uid-here": 0.72,
...etc,
}
How can I get the useEffect to trigger when the property that matches pvc.metadata.uid gets updated?
Or, how can I get the component to re-render when that value gets updated?
Quoting the docs:
The function passed to useEffect will run after the render is
committed to the screen.
And that's the key part (that many seem to miss): one uses dependency list supplied to useEffect to limit its invokations, but not to set up some conditions extra to that 'after the render is committed'.
In other words, if your component is not considered updated by React, useEffect hooks just won't be called!
Now, it's not clear from your question how exactly your context (progress) looks like, but this line:
setProgress(merge(progress, data.progress));
... is highly suspicious.
See, for React to track the change in object the reference of this object should change. Now, there's a big chance setProgress just assignes value (passed as its parameter) to a variable, and doesn't do any cloning, shallow or deep.
Yet if merge in your code is similar to lodash.merge (and, again, there's a huge chance it actually is lodash.merge; JS ecosystem is not that big these days), it doesn't return a new object; instead it reassigns values from data.progress to progress and returns the latter.
It's pretty easy to check: replace the aforementioned line with...
setProgress({ ...merge(progress, data.progress) });
Now, in this case a new object will be created and its value will be passed to setProgress. I strongly suggest moving this cloning inside setProgress though; sure, you can do some checks there whether or not you should actually force value update, but even without those checks it should be performant enough.
There seems to be no problem... are you sure pvc.metadata.uid key is in the progress object?
another point: move that dependency into a separate variable after that, put it in the dependency array.
Spread operator create a new reference, so it will trigger the render
let updated = {...property};
updated[propertyname] =value;
setProperty(()=>updated);
If you use only the below code snippet, it will not re-render
let updated = property; //here property is the base object
updated[propertyname] = value;
setProperty(()=>updated);
Try [progress['pvc.metadata.uid']]
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress['pvc.metadata.uid']])
return (
{/* stuff */}
);
}

set state in a callback of an async function

I am new to React, so bear with me please. I have a component that calls another component that takes a property. This property will get it's value on a callback of a function, something like this:
render(){
myFunc((p) => {
if(!_.isEqual(p, this.state.myProp))
this.setState({myProp: p})
});
return <MyComponent myProp={this.state.myProp}/>
}
myFunc will or will not make an API request and depending on that will call the callback sooner or later. This seems to work fine when API request is made and the callback takes longer to return. However, when the request is not needed and callback returns instantaneously (or almost) I am getting a Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
What am I doing wrong and what is the right way to approach this? Where would be the right place to put this code? Basically what I need is to re-render MyComponenent if this.state.myProp changes
You shouldn't be calling setState inside the render method, you might end up having an infinite loop.
The call to myFunc should be somewhere else (depending on the business logic you have). When the function finishes, it will update the state and then trigger a re-render so MyComponent will get the latest value.
UPDATE
I don't know which conditions will require calling myFunc again, but you can do:
state = {
myProp: null // or some other value that MyComponent can handle as a null state
}
componentDidMount () {
myFunc((p) => {
if(!_.isEqual(p, this.state.myProp)) // This is needed only if you get null back from the callback and you don't want to perform an unnecesary state update
this.setState({myProp: p})
}
}
render(){
const { myProp } = this.state
// You can also do if (!myProp) return null
return <MyComponent myProp={myProp}/>
}

Element-less component with bound events

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.

Throttling dispatch in redux producing strange behaviour

I have this class:
export default class Search extends Component {
throttle(fn, threshhold, scope) {
var last,
deferTimer;
return function () {
var context = scope || this;
var now = +new Date,
args = arguments;
if (last && now < last + threshhold) {
// hold on to it
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
fn.apply(context, args);
}, threshhold);
} else {
last = now;
fn.apply(context, args);
}
}
}
render() {
return (
<div>
<input type='text' ref='input' onChange={this.throttle(this.handleSearch,3000,this)} />
</div>
)
}
handleSearch(e) {
let text = this.refs.input.value;
this.someFunc();
//this.props.onSearch(text)
}
someFunc() {
console.log('hi')
}
}
All this code does it log out hi every 3 seconds - the throttle call wrapping the handleSearch method takes care of this
As soon as I uncomment this line:
this.props.onSearch(text)
the throttle methods stops having an effect and the console just logs out hi every time the key is hit without a pause and also the oSearch function is invoked.
This onSearch method is a prop method passed down from the main app:
<Search onSearch={ text => dispatch(search(text)) } />
the redux dispatch fires off a redux search action which looks like so:
export function searchPerformed(search) {
return {
type: SEARCH_PERFORMED
}
}
I have no idea why this is happening - I'm guessing it's something to do with redux because the issue occurs when handleSearch is calling onSearch, which in turn fires a redux dispatch in the parent component.
The problem is that the first time it executes, it goes to the else, which calls the dispatch function. The reducer probably immediately update some state, and causes a rerender; the re-render causes the input to be created again, with a new 'throttle closure' which again has null 'last' and 'deferTimer' -> going to the else every single time, hence updating immediately.
As Mike noted, just not updating the component can you get the right behavior, if the component doesn't need updating.
In my case, I had a component that needed to poll a server for updates every couple of seconds, until some state-derived prop changed value (e.g. 'pending' vs 'complete').
Every time the new data came in, the component re-rendered, and called the action creator again, and throttling the action creator didn't work.
I was able to solve simply by handing the relevant action creator to setInterval on component mount. Yes, it's a side effect happening on render, but it's easy to reason about, and the actual state changes still go through the dispatcher.
If you want to keep it pure, or your use case is more complicated, check out https://github.com/pirosikick/redux-throttle-actions.
Thanks to luanped who helped me realise the issue here. With that understood I was able to find a simple solution. The search component does not need to update as the input is an uncontrolled component. To stop the cyclical issue I was having I've used shouldComponentUpdate to prevent it from ever re-rendering:
constructor() {
super();
this.handleSearch = _.throttle(this.handleSearch,1000);
}
shouldComponentUpdate() {
return false;
}
I also moved the throttle in to the constructor so there can only ever be once instance of the throttle.
I think this is a good solution, however I am only just starting to learn react so if anyone can point out a problem with this approach it would be welcomed.

Resources