I'm using jQuery to create event bindings in a ReactJS component's componentDidMount function, which seems like the right place to do this.
$('body').on('defaultSearchContext.registerQueryEditor', (function(_this) {
return function(event, component) {
_this.setState({
queryEditors: _this.state.queryEditors.concat([component])
});
};
})(this));
This code isn't actually run on componentDidMount, it's simply setting up the binding that later calls setState when the event fires. However, this generates the following warning every time this event triggers, which pollutes my console with dozens of warnings:
Warning: setState(...): Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
I have tried moving the setState code to a separate function like onEvent and calling that from the binding in componentDidMount but the warning is still produced.
Ideally, I'd like to create the binding in the proper place, indeed, there is some issue with doing it in componentDidMount. If not, I'd like to know if it's possible to silence the warning, or whether I should perhaps file a bug for ReactJS itself. If it helps, I'm using ReactJS 0.14.3 (latest at this time).
This is similar to, but not the same as React Js onClick inside render. In that case, the solution is to return an anonymous function to onClick, but that doesn't seem applicable to my situation.
You are trying to coordinate events between independent components. This is a fairly standard thing to do, and it doesn't require DOM events. The standard practice for doing this in React is to use a store/dispatcher pattern like Redux or Flux (I personally prefer redux). However, if this is part of a larger, not-completely-React application, then this may not be possible. If it is just for a small piece of an React app, it may still be overkill.
All you need is an object to coordinate events. An event is just a collection of callbacks, possibly typed or keyed. This requires nothing more than an object shared between two places. DOM Events are overkill; jQuery is overkill. You just need to trigger a callback.
This is a VERY SIMPLE event coordinator.
let simpleEventCoordinator = {
callbacks: new Map(),
getHandler(eventKey) {
let handler = this.callbacks.get(eventKey);
if (!handler) {
handler = new Set();
this.callbacks.set(eventKey, handler);
}
return handler;
},
registerCallback(eventKey, callback) {
this.getHandler(eventKey).add(callback);
},
removeCallback(eventKey, callback) {
this.getHandler(eventKey).delete(callback);
},
trigger(eventKey, data) {
this.getHandler(eventKey).forEach(c => c(data));
}
Keep a map of callbacks, which will be nameOfEvent => callback(). Call them when asked. Pretty straightforward.
I know nothing about how your components are structured, but you said they are independent. Let's say they look like this:
React.render((
<div>
<QueryManager />
<button onClick={() => simpleEvent.trigger('event')}>{'Update'}</button>
</div>
), document.body);
This is all your component needs to handle this event
componentDidMount() {
simpleEvent.registerCallback('event', this.update);
}
componentWillUnmount() {
simpleEvent.removeCallback('event', this.update);
}
update() {
//do some stuff
}
I've put together a very simple codepen demonstrating this.
Looking at the source code of where that warning is coming from, it appears that if some reference is maintained before an update is about to happen, it throws that warning. So maybe the way your mixing the jQuery events and react is creating a memory leak? Its hard to say exactly because of the lack of surrounding code to your snippet what else could be going on.
Related
I am new to RxJs in general but am investigating a bug in some React code in which, upon an unrelated action, an old event seems to be emitted and rendered to a display error. Think if you had two buttons that generated two messages somewhere on screen, and clicking one button was showing the message for the other button.
Being new to RxJs I'm not positive where the problem lays. I don't see a single ReplaySubject in the code, only Obserables, Subjects, and BehaviourSubjects. So this is either misuse of an RxJs feature or just some bad logic somewhere.
Anyway I found the code with the related Observable and I'm not quite sure what this person was trying to accomplish here. I have read up on combineLatest, map, and pipe, but this looks like pointless code to me. Could it also be somehow re-emitting old events? I don't see dynamic subscriptions anywhere, especially in this case.
Tldr I don't understand the intent of this code.
export interface IFeedback {
id: number
text: string
}
export interface IFeedbackMessages {
message: IFeedback | undefined
}
feedback$ = new BehaviorSubject<IFeedback | undefined>(undefined)
feedbackNotifs$: Observable<IFeedbackMessages> = combineLatest([
feedback$
]).pipe(
map(([feedback]) => ({
feedback
})
))
I also found this which maybe be an issue. In the React component that displays this message, am I wrong but does it look like each time this thing renders it subscribes and then unsubscribes to the above Subject?
const FeedbackDisplay: React.FC () => {
const [feedbackNotifications, setFeedbackNotifications] = React.useState<IFeedbackMessages>()
React.useEffect(() =>
{
const sub = notification$.subscribe(setFeedbackNotifications)
return () => sub?.unsubscribe()
}, [notifications$])
}
Could it also be somehow re-emitting old events?
Yes, it probably is. BehaviorSubject has the unique property of immediately emitting the last value pushed to it as soon as you subscribe to it.
It's great when you want to model some persistent state value, and it's not good for events whose actual moment of occurrence is key. It sounds like the feedback messages you're working with fall into the second category, in which case Subject is probably a better choice.
does it look like each time this thing renders it subscribes and then unsubscribes to the above Subject?
Not exactly. useEffect accepts a callback, and within that callback you can optionally return a "cleanup" function. React will hang onto that function until the effect is triggered again, then it calls it to clean things up (which in this case consists of closing out the subscription) to make room for the next effect.
So in this case, the unsubscribe will only happen when the component is rendered with a new value for notifications$. Also worth pointing out that notifications$ will only change if it's either passed as a prop or created within the component function. If it's defined outside the function (imported from another file for example), you don't need to (and in fact should not) put it into useEffect's dependency array.
I've using "billboard.js": "^1.10.2", and react.js
I had searched billboard.js's documentation and found that onresize(), onresized() is attached on window and when I call
chart.destroy() then It removes every events being attached on window that being related with this library.
So I tested it without state update on onresize(), onresized() it successfully deleted all events, but when I did it with state update on onreszie(), onresized() events were still attached on window. As a result of this I think this issue happens not because of billboardjs, but reactjs.
Why is it? Any ideas?
//...
const [isResize, setIsResize] = useState(false);
const options = {
onresize(ctx) {
// "resize" keep prints even chart.destory() is called.
console.log("resize");
setIsResize(true);
},
onresized(ctx) {
setIsResize(false);
},
//...
<Chart
className="timelineChart"
options={options}
isResize={isResize}
ref={chartRef}
/>
//...
const options = {
onresize(ctx) {
// "resize" is no longer prints when chart.destory() is called.
console.log("resize");
},
onresized(ctx) {
},
//...
I believe you are attaching multiple event listeners. Each time the Page1 component re-renders, it attaches a new set of event listeners without cleaning up the old ones. What causes a re-render? State changes. That's why you are only seeing the issue once you add useState and setState.
You can verify this by checking the logs and noticing this not so helpful error:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
in Page1 (at App.js:13)
You'll need to modify the code related to attaching/detaching event listeners to avoid this. I'm not familiar with Billboard so I can only tell you where the problem is, not the exact spot to fix it.
My gut says its the Chart.jsx here:
renderChart = () => {
console.log('render chart');
const { current } = this.chartRef;
if (current !== null) {
this.chartInstance = bb.generate({
...this.props.options,
bindto: this.chartRef.current
});
}
};
Updated with full solution
I was correct in that the Chart.jsx is where the problem lies.
Listeners should be attached when the DOM is created and removed with the DOM is destroyed. You were not wrong when you first thought to use the React Lifecycles for this chore, however I find the Callback References to be more useful, especially when some of the DOM may be destroyed or created during update cycles (you do not have this problem).
Callback References can be tricky, do not use functions that get recreated each render (trust me its a headache). Callback References are called for two reasons. First, the DOM has been created and to hand you a reference to the element. Second, the DOM has been destroyed, so time to clean up. If React senses a change in the Callback Reference (i.e. you give it a new one) it will tell the first Callback Reference to clean up, and the second Callback Reference to initialize (this is the headache I mentioned). You can avoid this by using an instance method.
// Bind to 'this' otherwise 'this' is lost
setChartRef = (ref) => {
// Remove listeners
if (!ref) {
console.log('no reference');
this.chartRef.current = null;
this.destroy();
}
// Add listeners
else {
console.log('new reference');
this.chartRef.current = ref;
this.createChart();
}
};
Next piece of the puzzle, you only want to call bb.generate one time. This was causing multiple listeners to be created. So I've simplified and renamed your renderChart to createChart
createChart = () => {
this.chartInstance = bb.generate({
...this.props.options,
bindto: this.chartRef.current
});
};
Finally, none of the lifecycle methods are necessary because Callback Reference tell us exactly when to create the chart and when to destroy it. You may be wondering what about resizing the chart? Seems like that is taken care of automatically? I could be missing something here, but in the event you need to update the chart, use this.chartInstance in an update lifecycle method.
My full modifications here:
onresize is actually a DOM event, it's not part of billboard or React.
https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event
You'll need to cancel your function somehow, like making it run conditionally or defining it in a component that will be unmounted.
I'm trying to clarify the pros/cons of having a new function declaration within react's render method.
Consider a render method like the following:
render () {
return (<SomeComponent somePropWithoutEventBind={() => console.log('not so dangerous?')} />)
}
In the above example, somePropWithoutEventBind doesn't bind to a DOM event: react will check for prop changes and every time render is called this prop has changed - because it's a new function - so it never matches the previous, this is expensive but nothing tremendous.
Now in this case
render () {
return (<input onChange={() => console.log('dangerous?')} />)
}
onChange prop does bind to DOM (doing something like addEventListener) so every render will have to removeEventListener and addEventListener again? Is this the main reason behind avoiding to declare functions inside the render method?
If possible, please justify your answer pointing to react source code.
The main reason of avoiding defining new functions in render is to avoid over rendering.
Consider bind a new function onto a DOM element (react element not real DOM) like so: <button onClick={_ => this.setState({ hide: true })}>Hide Me</button>} there's almost none cost at all, since DOM elements gets re-rendered anyways. (site note: react doesn't use native DOM events like add/removeEventListener, it uses SyntheticEvent and your code targets virtual DOM aka react element not real DOM)
However for a custom components (In large codebase we typically have lots of complex Container Component composed of Functional/Class Child Components. Let's say you have something like
render() {
// you won't run into unnessary re-render issue
// when you use `onClick={this.handleClick}` since the function reference doesn't change
// while most perf tricks done by react bindings are relying on shallow compare of props/state
return (
<ComplexContainer onClick={_ => this.setState({ forceReRender: true})}>
<Child1 />
<Child2>
<NestedChild1 />
<NestedChild2 />
</Child2>
</ComplexContainer>
)
}
If you do this way, this will cause the whole render tree starting from ComplexContainer to re-render, this may have notable negative perf impacts, but you will need DevTools profiling to benchmark.
In fact, the real thing i wanna say is: it might not be that huge as you concern, avoid premature optimization can be more important. Give this awesome reading material a shot: React, Inline Functions, and Performance
A bit more info regarding react synthetic event system here, it's simply a wrapper of native DOM events to normalize the subtle differences of events among different browser vendors. The API would be the same event.preventDefault()/event.preventPropagation() etc works as it is, but you get cross-browser compatibility for free. Regarding how it works internally please see event delegation
I'm trying to use the second parameter of setState to pass a callback function, but it appears (from what I can gather) that the server-side renderer ignores this parameter completely. I'm using Gatsby which utilizes server-side rendering to build a static React-based site. My call is in an onChange handler, and looks like this:
this.setState({ [event.target.name]: event.target.value }, () => { console.log('setState callback') })
The state is updated as expected, but the callback is never called. Note: The same issue applies whether I pass an object or a function for the first parameter. The component function looks like this:
ReactComponent.prototype.setState = function (partialState, callback) {
[...]
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
That updater's method, which lives in ReactUpdateQueue.js (according to the call stack) looks like this:
enqueueSetState: function (publicInstance, partialState)
I don't fully understand the build process for React, but I believe that method/file is coming from this file in the source:
/src/renderers/shared/server/ReactPartialRenderer.js
The only other place I can find this function defined is here:
/src/isomorphic/modern/class/ReactNoopUpdateQueue.js
enqueueSetState: function(
publicInstance,
partialState,
callback,
callerName,
) {
warnNoop(publicInstance, 'setState');
}
which looks like the correct method signature, but doesn't appear anywhere in the call stack when I debug the setState call in my code. This doesn't appear to be a problem with client-side rendered React components (I'll try to set up a simple repo to show this issue, but it doesn't appear replicable on CodePen etc.) I know I could use componentDidUpdate to accomplish what I need to do, but the callback is much more convenient in my instance, and I hate leaving a mystery like this unsolved. :)
Well, I figured it out, and turns out, as is too often the case, this was a self-inflicted error. The default Gatsby install uses React v15 but we wanted to use 16, so we added a direct dependency to it in package.json, which got built in to the resulting package. I still don't quite understand why the above mentioned version of enqueueSetState was called instead of the proper one, but removing the reference to react (and adding gatsby-plugin-react-next, which does what we want by simply pointing webpack to the newer version) fixed the issue.
At least this was a good excuse to get a little more familiar with the guts of React. Perhaps this will save somebody else some time in the future.
I render a React component SettingsTab within a wrapper called TeamView. Its API looks something like
class TeamView {
constructor() {
this.el = document.createElement('div');
}
render() {
ReactDOM.render(<SettingsTab/>, this.el);
return this;
}
remove() {
this.el.remove();
}
}
used something like
// to present the team view
const teamView = new TeamView();
document.body.appendChild(teamView.render().el);
// to remove the team view
teamView.remove();
And what I'm wondering is, should TeamView#remove call ReactDOM. unmountComponentAtNode(this.el) before calling this.el.remove()?
The examples I can find around the web make it seem like unmountComponentAtNode only needs to be called if the container is going to remain in the DOM; and the new portals example just removes the container, without calling unmountComponentAtNode.
But, I'm not sure if that's special because it's using a portal, and this post makes it kind of seem like it's always good practice to call unmountComponentAtNode.
Yes, it is important to call unmountComponentAtNode() because if you don't do this, none of the components below in the tree will know they have been unmounted.
User-defined components often do something in componentDidMount that creates a reference to the tree from the global environment. For example, you may add a window event handler (which isn't managed by React), a Redux store subscription, a setInterval call, etc. All of this is fine and normal as long as these bindings are removed in componentWillUnmount.
However, if you just remove the root from the DOM but never call unmountComponentAtNode, React will have no idea the components in that tree need to be unmounted. Since their componentWillUnmount never fires, those subscriptions stay, and prevent the whole tree from getting garbage collected.
So for all practical purposes you should always unmount the root if you're going to remove that container node. Otherwise you'll most likely get a memory leakāif not now, then later when some of your components (potentially deep in the tree, maybe even from third-party libraries) add subscriptions in their componentDidMount.
Even though you called this.el.remove(), you should still call the unmountComponentAtNode(this.el) because unmountComponentAtNode will clean up its event handlers and state, but the remove method will not.
For example, Eventhough you have clicked to remove the div, you can still call it's click event handlers:
var tap = document.querySelector('.tap');
var other = document.querySelector('.other');
tap.addEventListener('click', function(e) {
console.log(tap.getAttribute('data-name') + ' has been clicked');
tap.remove();
});
other.addEventListener('click', function(e) {
tap.click();
});
<div class="tap" data-name="tap">First Click me to remove me</div>
<div class="other">Then Click me </div>
I asked this question in the #react-internals Discord channel and received the following response:
So, this tallies with what #jiangangxiong says above: as long as we
don't keep our own references to component DOM elements
nor attach event handlers outside of React
and only need to support modern browsers
we should only need to remove the container to have the component's event handlers and state garbage collected, no need to call unmountComponentAtNode.