Difference between init() and onRender() lifecycle hooks in grapesjs? - grapesjs

Grapesjs provide two lifecycle methods: init() and onRender(), I am actually quite confused with those two hooks:
As the doc said:
Local hook: view.init() method, executed once the view of the component is initiliazed
Local hook: view.onRender() method, executed once the component is rendered on the canvas
init({ model }) {
// Do something in view on model property change
this.listenTo(model, 'change:prop', this.handlePropChange);
// If you attach listeners on outside objects remember to unbind
// them in `removed` function in order to avoid memory leaks
this.onDocClick = this.onDocClick.bind(this);
document.addEventListener('click', this.onDocClick)
},
// Do something with the content once the element is rendered.
// The DOM element is passed as `el` in the argument object,
// but you can access it from any function via `this.el`
onRender({ el }) {
const btn = document.createElement('button');
btn.value = '+';
// This is just an example, AVOID adding events on inner elements,
// use `events` for these cases
btn.addEventListener('click', () => {});
el.appendChild(btn);
},
For example, i can access this.el in both methods to get dom element. if I want to attach a event listener on this.el, which one is more appropriate to do such operation?
In general, what's difference between those two methods, and in what scenario should i use them?

Use onRender when you need the element in the DOM before the hook executes.
Example:
var el = document.createElement('DIV');
el.style = 'height: 10px';
// logs 0
console.log(el.clientHeight);
document.body.appendChild(el);
// logs 10
console.log(el.clientHeight);
clientHeight returns the height of the element in the DOM. It does not compute the height of the element if it is not already in the DOM. There are many properties and functions for HTML elements for which this is relevant.
Use init when you want your hook to execute as soon as the component initializes without waiting for the render.
This can be helpful for setting up event listeners. If you set up event listeners in onRender, then any events that fire after init and before onRender won't be caught.
If the code in your hook doesn't need to be called as soon as the component is initialized and is not dependent on the element being in the DOM, then it doesn't really matter which you choose. In most cases these events will be milliseconds apart.
I would typically lean towards init though so the hook executes ASAP and doesn't wait around if there is an issue with rendering.

Related

How to ensure parent component passes the same prop function to child to avoid rerender

Currently I have a two component set up, where the parent renders some data and handles retrieval and the child is a filter. This filter allows the user to filter by status or keyword. Nothing fancy.
Now this is a paginated system. After the parent makes an initial request for data, they're given the next page ID to request if they want more. But if the filter is updated, this next page ID needs to be wiped out, as it's no good.
So what I do is pass a function from the parent to the child called updateFilter(). If the filter component has an update in state, it calls up to the parent and runs updateFilter(). One of the values updated is included in a useEffect() dependency array, so the parent then requests the new data with the new filters. Easy.
The problem is in setting up the child's useEffect(). Eslint tells me I need to add props.updateFilter to the dependency array, and while I can just ignore this, I feel that it would be wrong to do so. But the parent has a fair bit of state that will update, and when it does, it passes a new copy of updateFilter() down into the child which causes it to incorrectly trigger.
How do I go about fixing this? Can I tell the child to only use a static version of this function somehow? Or do I just exclude props.updateFilter from the dependency array? Below is a rough psuedo code of my components.
Parent {
const [stateVal, setStateVal] = useState(...);
function updateFilter(filterStatus) {
...
setStateVal(filterStatus);
}
useEffect(() => ..., [stateVal]);
return <Child updateFilter={updateFilter} />
}
Child {
const [filterStatus, setStatus] = useState(...);
useEffect(() => {
props.updateFilter(filterStatus);
}, [filterStatus] // Adding `props` here is what I think I should do, but that causes the issue. Apparently the `props` val changes every time Parent's state changes
return ( ... );
}
How do I go about fixing this? Can I tell the child to only use a
static version of this function somehow?
Yep!
So, if props.updateFilter is included in the dependency array, you have issues. You call that function and it causes your parent component to re-render. And guess what? The parent creates a new updateFilter function (it does the same thing, but it makes a new one, the reference to the function is a new value which is all React checks). This causes the child to re-render, which causes your useEffect to run because its dependency is a new function. That's bad!
So... add useCallback
function updateFilter = useCallback((filterStatus) => {
...
setStateVal(filterStatus);
}, []);
useCallback creates the function one time, and only makes a new reference if its dependencies change (as it should). It has a little bit more overhead, but if I'm ever unsure I use it.
Also, bonus, after dealing with these issues, I use the setState(previousValue => previousValue + 1) form much more than setState(previousValue + 1) as it has many benefits. previousValue doesn't have to be in the dependency array and multiple setStates can be stacked in one render cycle (instead of using the initial value).

Window events still alive even it is deleted

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.

useState React hook always returning initial value

locationHistory is always an empty array in the following code:
export function LocationHistoryProvider({ history, children }) {
const [locationHistory, setLocationHistory] = useState([])
useEffect(() => history.listen((location, action) => {
console.log('old state:', locationHistory)
const newLocationHistory = locationHistory ? [...locationHistory, location.pathname] : [location.pathname]
setLocationHistory(newLocationHistory)
}), [history])
return <LocationHistoryContext.Provider value={locationHistory}>{children}</LocationHistoryContext.Provider>
}
console.log always logs []. I have tried doing exactly the same thing in a regular react class and it works fine, which leads me to think I am using hooks wrong.
Any advice would be much appreciated.
UPDATE: Removing the second argument to useEffect ([history]) fixes it. But why? The intention is that this effect will not need to be rerun on every rerender. Becuase it shouldn't need to be. I thought that was the way effects worked.
Adding an empty array also breaks it. It seems [locationHistory] must be added as the 2nd argument to useEffect which stops it from breaking (or no 2nd argument at all). But I am confused why this stops it from breaking? history.listen should run any time the location changes. Why does useEffect need to run again every time locationHistory changes, in order to avoid the aforementioned problem?
P.S. Play around with it here: https://codesandbox.io/s/react-router-ur4d3?fontsize=14 (thanks to lissitz for doing most the leg work there)
You're setting up a listener for the history object, right?
Assuming your history object will remain the same (the very same object reference) across multiple render, this is want you should do:
Set up the listener, after 1st render (i.e: after mounting)
Remove the listener, after unmount
For this you could do it like this:
useEffect(()=>{
history.listen(()=>{//DO WHATEVER});
return () => history.unsubscribe(); // PSEUDO CODE. YOU CAN RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[]); // THIS EMPTY ARRAY MAKES SURE YOUR EFFECT WILL ONLY RUN AFTER 1ST RENDER
But if your history object will change on every render, you'll need to:
cancel the last listener (from the previous render) and
set up a new listener every time your history object changes.
useEffect(()=>{
history.listen(()=>{//DO SOMETHING});
return () => history.unsubscribe(); // PSEUDO CODE. IN THIS CASE, YOU SHOULD RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[history]); // THIS ARRAY MAKES SURE YOUR EFFECT WILL RUN AFTER EVERY RENDER WITH A DIFFERENT `history` OBJECT
NOTE: setState functions are guaranteed to be the same instance across every render. So they don't need to be in the dependency array.
But if you want to access the current state inside of your useEffect. You shouldn't use it directly like you did with the locationHistory (you can, but if you do, you'll need to add it to the dependency array and your effect will run every time it changes). To avoid accessing it directly and adding it to the dependency array, you can do it like this, by using the functional form of the setState method.
setLocationHistory((prevState) => {
if (prevState.length > 0) {
// DO WHATEVER
}
return SOMETHING; // I.E.: SOMETHING WILL BE YOUR NEW STATE
});

Is it necessary to call `unmountComponentAtNode` if the component's container is removed?

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.

Binding to event handler that calls setState in ComponentDidMount produces warning

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.

Resources