React + Firebase Component does not re-render on data change (remove) - javascript

This is a rendering / performance problem.
I've built a basic React App using Firebase. It basically works, with one notable anomalous behavior: rendering doesn't occur entirely on its own.
Specifically, I retrieve a list of groups to which a user belongs, and want to be able to add/remove groups, triggering a re-render. I'm using Firebase's .on('value'), .on('child_added'), and .on('child_removed') functions to bind references to these events.
Adding and removing groups works, however the group list disappears (especially noticed on remove), and I have to to click on a button (literally, any button in the UI) to bring it back (it pops up immediately, with all the correct data based on user action).
So - I'm clearly missing something somewhere, although I've been testing various fixes and have yet to be able to understand the issue. Components are:
Main Admin Component:
var React = require('react'),
groupRef = new Firebase('http://my-url/groups'), //list of groups
userRef = new Firebase('http://my-url/users'), //list of users
groupListArray = [];
var AdminContainer = React.createClass({
mixins: [ReactFireMixin],
getInitialState: function() {
return {
groups: []
}
},
buildGroupList: function (dataSnapshot) {
groupListArray = [];
var groupList = dataSnapshot.val();
/* The group ids are saved as children in the db
* under users/$userid/groups to reflect which groups a user can
* access - here I get the group ids and then iterate over the
* actual groups to get their names and other associated data under
* groups/<misc info>
*/
for (var key in groupList) {
if (groupList.hasOwnProperty(key)) {
groupRef.child(key).once('value', function(snapShot2) {
groupListArray.push(snapShot2);
});
}
}
this.setState({
groups: groupListArray
});
},
componentWillMount: function() {
userRef.child(auth.uid).child('groups').on('value', this.buildGroupList);
userRef.child(auth.uid).child('groups').on('child_removed', this.buildGroupList);
userRef.child(auth.uid).child('groups').on('child_added', this.buildGroupList);
},
handleRemoveGroup: function(groupKey){
userRef.child(auth.uid).child('groups').child(groupKey).remove();
},
render: function() {
<div id="groupAdminDiv">
<table id="groupAdminTable">
<thead>
<tr>
<th>Group Name</th>
<th>Action</th>
</tr>
</thead>
<GroupList groups={this.state.groups} remove={this.handleRemoveGroup} />
</table>
</div>
}
});
module.exports = AdminContainer;
Then the GroupList Component:
var React = require('react');
var GroupList = React.createClass({
render: function() {
var listGroups = this.props.groups.map((group, index) => {
if (group != null) {
return (
<tr key={index} className="u-full-width">
<td>
{group.val().group_name}
</td>
<td>
<button className="button" onClick={this.props.remove.bind(null, group.key())}>Remove</button>
</td>
</tr>
)
} else {
return (
<tr>
<td>You have not added any Groups.</td>
</tr>
)
}
});
return (
<tbody>
{listGroups}
</tbody>
)
}
});
module.exports = GroupList;
Any help much appreciated!!

You appear to already understand what you need. In your componentWillMount method you register for various userRef events. You just need to register for the groupRef events, using the same callback. React re-renders whenever a components setState method is called, which you are doing inside buildGroupList. You just need to call buildGroupList after the groupRef updates.
componentWillMount: function() {
var events = ['value', 'child_removed', 'child_added'];
var refs = [groupRef, userRef.child(auth.uid).child('groups')];
var callback = this.buildGroupList;
refs.forEach(function(ref) {
events.forEach(function(e) {
ref.on(e, callback);
});
});
},

I believe this was caused by use of .once(). As per Firebase's documentation, .once() is used to query a database location without attaching an enduring listener such as .on('value', callBack). However, when I changed the instances in my code where I was calling .once() to use .on() (even though I didn't want to attach a listener there but instead simply retrieve data one time), all the erratic behavior stopped and my app updated this.state and components as I had originally expected it to.
The only thing I can take away from this experience is that .once() does not function as intended / stated and that .on() (plus appropriate .off() references) should be used instead.

Related

How to bind a react hook to an element created by jquery function

I'm working with a project written in jquery and I want to integrate this code in my react project. I'm trying to send a query to my graphql server by clicking a button.
There is a JQ function that creates multiple elements like this:
canvas.append($(`<div class="d-flex flex-wrap my-2"><img class="else_picture" src="${elem.artworkUrl600}" height="150px"><div class="mx-2"><div class="elseTitle" id="${elem.trackId}">${elem.trackName}</div><h6>by:${elem.artistName}</h6><h6>keywords:</h6><p>${elem.genres}</p><div class="d-flex justify-content-start"><button value="${elem.feedUrl}" class="get_description"><a target="_blank" href="${elem.trackViewUrl}">Info link</a></i></button><div class="like_box"><i id="like" class="fa fa-thumbs-up"></div></div></div><hr>`)
);
My goal is to connect a function "onClick()" to all buttons inside these created elements. For that I'm trying to define a function that would query all elements by id and connect it to a function with a hook to my graphql server:
function connectFunctionalityToLike(){
$("#like").on("click", function(){ Like(this); });
}
function Like(x){
console.log("clicked");
const { loading, error, data } = useQuery(ME_QUERY);
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
console.log(data);
}
My function Like() does not really work because I'm missing in understanding how elements in react actually work all together with jquery. I cannot rewrite code in JQuery. I just want to integrate this existing code. Is there any way around connecting react hooks to created elements by id?
Is there
You should create a React component wrapper for the JQuery function.
You will have to manage the React lifecycle manually - by calling the JQuery function in a useEffect() callback and taking care to remove the DOM elements in the cleanup:
function ReactWrapper() {
const { loading, error, data } = useQuery(ME_QUERY);
const [ data, setData ] = React.useState();
React.useEffect(() => {
jqueryFn();
$('#like').on('click', function(){ setData('from click') });
return () => {
/* code that removes the DOM elements created by jqueryFn() */
};
}, []);
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
return <div>{data}</div>;
}
Then, in this React component, will be allowed to use hooks.
Also, if your JQuery function is so simple, you are probably much better off with rewriting it in React.
As Andy says in his comment: "You shouldn't be mixing jQuery and React." - But, we've all be in spots where we are given no choice. If you have no choice but to use jQuery along side React, the code in the following example may be helpful.
Example is slightly different (no canvas, etc), but you should be able to see what you need.
This example goes beyond your question and anticipates the next - "How can I access and use the data from that [card, div, section, etc] that was added by jQuery?"
Here is a working StackBlitz EDIT - fixed link
Please see the comments in code:
// jQuery Addition - happens outside the React App
$('body').append(
$(`<div class="d-flex flex-wrap my-2"><img class="else_picture" src="${elem.artworkUrl600}" height="10px"><div class="mx-2"><div class="elseTitle" id="${elem.trackId}">${elem.trackName}</div><h6>by:${elem.artistName}</h6><h6>keywords:</h6><p>${elem.genres}</p><div class="d-flex justify-content-start"><button value="${elem.feedUrl}" class="get_description"><a target="_blank" href="${elem.trackViewUrl}">Info link</a></i></button><div class="like_box"><i id="like" class="fa fa-thumbs-up"></div></div></div><hr>`)
);
export default function App() {
// state to hold the like data
const [likes, setLikes] = useState([]);
// create reference to the jquery element of importance
// This ref will reference a jQuery object in .current
const ref = useRef($('.elseTitle'));
// Like function
function Like(x) {
// ref.current is a jQuery object so use .text() method to retrieve textContent
console.log(ref.current.text(), 'hit Like function');
setLikes([...likes, ' :: ' + ref.current.text()]);
}
// This is where you put
useEffect(() => {
// Add event listener and react
$('#like').on('click', function () {
Like(this);
});
// Remove event listener
return () => $('#like').off('click');
});
return (
<div>
<h1>Likes!</h1>
<p>{likes}</p>
</div>
);
}

VueJs manipulate inline template and reinitialize it

this question is similar to VueJS re-compile HTML in an inline-template component and also to How to make Vue js directive working in an appended html element
Unfortunately the solution in that question can't be used anymore for the current VueJS implementation as $compile was removed.
My use case is the following:
I have to use third party code which manipulates the page and fires an event afterwards. Now after that event was fired I would like to let VueJS know that it should reinitialize the current DOM.
(The third party which is written in pure javascript allows an user to add new widgets to a page)
https://jsfiddle.net/5y8c0u2k/
HTML
<div id="app">
<my-input inline-template>
<div class="wrapper">
My inline template<br>
<input v-model="value">
<my-element inline-template :value="value">
<button v-text="value" #click="click"></button>
</my-element>
</div>
</my-input>
</div>
Javascript - VueJS 2.2
Vue.component('my-input', {
data() {
return {
value: 1000
};
}
});
Vue.component('my-element', {
props: {
value: String
},
methods: {
click() {
console.log('Clicked the button');
}
}
});
new Vue({
el: '#app',
});
// Pseudo code
setInterval(() => {
// Third party library adds html:
var newContent = document.createElement('div');
newContent.innerHTML = `<my-element inline-template :value="value">
<button v-text="value" #click="click"></button>
</my-element>`; document.querySelector('.wrapper').appendChild(newContent)
//
// How would I now reinialize the app or
// the wrapping component to use the click handler and value?
//
}, 5000)
After further investigation I reached out to the VueJs team and got the feedback that the following approach could be a valid solution:
/**
* Content change handler
*/
function handleContentChange() {
const inlineTemplates = document.querySelector('[inline-template]');
for (var inlineTemplate of inlineTemplates) {
processNewElement(inlineTemplate);
}
}
/**
* Tell vue to initialize a new element
*/
function processNewElement(element) {
const vue = getClosestVueInstance(element);
new Vue({
el: element,
data: vue.$data
});
}
/**
* Returns the __vue__ instance of the next element up the dom tree
*/
function getClosestVueInstance(element) {
if (element) {
return element.__vue__ || getClosestVueInstance(element.parentElement);
}
}
You can try it in the following fiddle
Generally when I hear questions like this, they seem to always be resolved by using some of Vue's more intimate and obscured inner beauty :)
I have used quite a few third party libs that 'insist on owning the data', which they use to modify the DOM - but if you can use these events, you can proxy the changes to a Vue owned object - or, if you can't have a vue-owned object, you can observe an independent data structure through computed properties.
window.someObjectINeedtoObserve = {...}
yourLib.on('someEvent', (data) => {
// affect someObjectINeedtoObserve...
})
new Vue ({
// ...
computed: {
myObject () {
// object now observed and bound and the dom will react to changes
return window.someObjectINeedtoObserve
}
}
})
If you could clarify the use case and libraries, we might be able to help more.

Issues with updating the State - React

I'm having issues in updating the state values, I'm rendering a external component using Map, and hence not able to access this. So on click of the component I'm not able to call the handleClick function to update the state values..
Here is the state :
this.state = {
attributes : {
hours : {
},
cost : 0,
amenities : defaultAmenities
},
primary_category : "General"
}
Where defaultAmenities is a external file with large javascript object.
The render function :
render() {
let basicAmenities, extendedAmenities
let basicAmenitiesList = [], extendedAmenitiesList = []
//Wrong way of storing this
let _this = this;
}
... More Logics / Switch Cases ...
let amenitiesList = basicAmenitiesList.map(function(item, index){
return <Attribute key={index} name={item.amenity_id} type={item.title} icon={item.icon} selected={item.isSelected} value="" onClick={_this.handleClick.bind(_this)}/>
})
And the attribute component
<div className="attribute-grid" onClick={this.props.onClick}>
...
</div>
Handle click is a function to setState on click of Attribute.
handleClick(e) {
console.log(e.target);
}
On click of the attribute, I need to update the state. The result of console log is attached below. I need to target the input values, but since it return the entire div, how do i get the values of name/value/placeholder?
<div class="attribute-grid-block" data-reactid=".0.2.0.3.0.1.$0.0"><div class="attribute-grid-img" data-reactid=".0.2.0.3.0.1.$0.0.0"><img src="petsIcon" data-reactid=".0.2.0.3.0.1.$0.0.0.0"></div><div class="attribute-grid-info" data-reactid=".0.2.0.3.0.1.$0.0.1"><h6 data-reactid=".0.2.0.3.0.1.$0.0.1.0">Pets</h6><input type="text" name="pets" placeholder="NO INFO FOUND" value="" disabled="" data-reactid=".0.2.0.3.0.1.$0.0.1.1"></div></div>
you can get what you need from the target. but you need to set the onClick on the element that you want it to be the target and then you will have it:
handleClick(e) {
const name = e.target.name;
const value = e.target.value;
const placeholder = e.target.placeholder;
console.log(placeholder);
}
if you want to set the onClick elsewhere you will need to send the values you want, so inside Attribute component you will have a function that will be invoke on click and call the this.props.onClick({ name: '', value: ''});
if you need to use this inside this function, and you are using react with classes. you can write this:
handleClick = (e) => {
console.log(this);
}

React js: Invariant Violation: processUpdates() when rendering a table with a different number of child rows

I am getting the following error when my react component is re-rendered after a click event:
Uncaught Error: Invariant Violation: processUpdates(): Unable to find child 2 of element. This probably means the DOM was unexpectedly mutated ...
This only happens when my table has a different number of rows than the previously rendered version. For example:
/** #jsx React.DOM */
React = require('react');
var _ = require("underscore");
var testComp = React.createClass({
getInitialState: function () {
return {
collapsed: false
};
},
handleCollapseClick: function(){
this.setState({collapsed: !this.state.collapsed});
},
render: function() {
var rows = [
<tr onClick={this.handleCollapseClick}><th>Header 1</th><th>Header 2</th><th>Header 3</th></tr>
];
if(!this.state.collapsed){
rows.push(<tr><th>Row1 1</th><th>Row1 2</th><th>Row1 3</th></tr>);
}
rows.push(<tr><th>Footer 1</th><th>Footer 2</th><th>Footer 3</th></tr>);
return <div>
<table>
{rows}
</table>
</div>
}
});
module.exports = testComp
If I render different content, but with the same number of rows, I don't get the error, so if I update the if statement to:
if(!this.state.collapsed){
rows.push(<tr><th>Row1 1</th><th>Row1 2</th><th>Row1 3</th></tr>);
}else{
rows.push(<tr><th>Row2 1</th><th>Row2 2</th><th>Row2 3</th></tr>);
}
... everything works.
Do I need to force react to re-render the entire component in this case, instead of just the 'changed' elements?
You should read the full error message (at least that's what I am seeing):
Unable to find child 2 of element. This probably means the DOM was unexpectedly mutated (e.g., by the browser), usually due to forgetting a <tbody> when using tables, nesting tags like <form>, <p>, or <a>, or using non-SVG elements in an parent.
Every table needs a <tbody> element. If it doesn't exist, the browser will add it. However, React doesn't work if the DOM is manipulated from the outside.
Related: Removing row from table results in TypeError
I encountered this when dealing with p tags. I had nested paragraphs within paragraphs.
To resolve the problem I replaced the wrapping p element with a div element.
Before:
render: function render() {
return (
<p>
<p>1</p>
<p>2</p>
</p>
);
}
After:
render: function render() {
return (
<div>
<p>1</p>
<p>2</p>
</div>
);
}
For people who are using react-templates:
You have to generate the <tbody> tag in the .jsx file. If you only add it in the .rt file you still get the error message.
this.tbody = <tbody>{tablerows}</tbody> // - in .jsx
In My case the fix is to remove spaces in Table render from
<tbody> {rows} </tbody>
to
<tbody>{rows}</tbody>
I think your problem is with using the <th> tag in multiple rows. <th> is reserved for the header row. Try replacing it with <td> everywhere except in the first row. Also you should wrap the header in a <thead> tag and the rest in a <tbody> tag:
var header = [
<tr onClick={this.handleCollapseClick}><th>Header 1</th><th>Header 2</th><th>Header 3</th></tr>
];
var body = [];
if(!this.state.collapsed){
body.push(<tr><td>Row1 1</td><td>Row1 2</td><td>Row1 3</td></tr>);
}
body.push(<tr><td>Footer 1</td><td>Footer 2</td><td>Footer 3</td></tr>);
return <div>
<table>
<thead>{header}</thead>
<tbody>{body}</tbody>
</table>
</div>;

ReactJS and non-hierarchical components

I've been working on a context menu module in ReactJS, and it's got me thinking about how to deal with non-hierarchical components.
The problem I'm running into is that many different items in an application may want to use a context menu. Normally in React, you pass a callback from a parent object to the children that need to communicate with the parent. For example, my first thought was to have an openContextMenu(mousePosition, optionsObject) function passed from my ContextMenu class to all the elements that want to display a context menu on right-click.
But it doesn't make sense for all such elements (or maybe even any) to be children of a context menu! The context menu is not hierarchical with respect to other components of the application. In Angular, I would probably write a ContextMenu service that components required if they wanted access to such a menu.
Is this a situation in which a global event handler should be used? Am I thinking about this all wrong? What's the React way to handle this kind of horizontal interaction between components?
Context menus are special. There should never be more than one context menu open at any time. They're also special because it can be opened from anywhere. Try the demo to get an idea of how this looks when put together.
To solve our global problem, we'll create a mixin which wraps a private event emitter.
var menuEvents = new events.EventEmitter();
var ContextMenuMixin = {
// this.openContextMenu(['foo', 'bar'], (err, choice) => void)
openContextMenu: function(options, callback){
menuEvents.emit('open', {
options: options,
callback: callback
});
},
closeContextMenu: function(){
menuEvents.emit('close');
}
};
Now for the component, we need to do a few things. Here's the initialization part. Just binding to some events, and lightweight mouse tracking.
var mouse = {x: 0, y: 0};
var updateMouse = function(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
};
var ContextMenu = React.createClass({
getInitialState: function(){
return {options: null, callback: null};
},
componentDidMount: function(){
menuEvents.addListener('open', this.handleOpenEvent);
menuEvents.addListener('close', this.closeMenu);
addEventListener('mousemove', updateMouse);
},
These event handlers are very simple. handleOpenEvent just stores the event payload and mouse position in state, which effectively locks in the mouse position until it's opened next. And the counterpart simply resets the state, and calls the callback with an error.
handleOpenEvent: function(payload){
this.setState(_.merge({}, payload, mouse));
},
closeMenu: function(){
if (this.state.callback) {
this.replaceState(this.getInitialState());
this.state.callback(new Error('no selection made'));
}
},
And finally, we render a list of options passed to the event, and we create click handlers for each.
render: function(){
if (!this.state.options) {
return <div />
}
var style = {
left: this.state.x,
top: this.state.y,
position: 'fixed'
};
return (
<div className="react-contextmenu" style={style}>
<ul className="react-contextmenu-options">
{this.state.options.map(function(x, i){
return <li key={i}
onClick={this.makeClickHandler(x)}>
{x}
</li>
}, this)}
</ul>
</div>
);
},
makeClickHandler: function(option){
return function(){
if (this.state.callback) {
this.state.callback(null, option);
this.replaceState(this.getInitialState());
}
}.bind(this);
}

Categories

Resources