Component not re-rendering when nested observable changes - reactjs

Adding a node to a list and yet a component is not re-rendering. Mobx Chrome Extension dev tools says it's a dependency but for some reason still no reaction!
A button renders 'Add' or 'Remove' based on whether a node is in a list. It doesn't re-render unless I move to another component and then open this component again.
Buttons:
#inject("appStore") #observer
class EntityTab extends Component {
...
render() {
return (
...
{/* BUTTONS */}
{ this.props.appStore.repo.canvas.graph.structure.dictionary[id] !== undefined ?
<div onClick={() => this.props.appStore.repo.canvas.graph.function(id)}>
<p>Remove</p>
</div>
:
<div onClick={() => this.props.appStore.currRepo.canvas.otherfunction(id)}>
<p>Add</p>
</div>
}
...
)
}
}
The Add button renders, I click on the button which triggers
this.props.appStore.currRepo.canvas.otherfunction(id)
and then we go to this function
#observable graph = new Graph();
...
#action
otherfunction = (idList) => {
// Do nothing if no ids are given
if (idList === null || (Array.isArray(idList) && idList.length === 0)) return;
// If idList is a single value, wrap it with a list
if (!Array.isArray(idList)) { idList = [idList] }
let nodesToAdd = [];
let linksToAdd = [];
// Add all new links/nodes to graph
Promise.all(idList.map((id) => { return this.getNode(id, 1) }))
.then((responses) => {
for (let i = 0; i < responses.length; i++) {
let res = responses[i];
if (res.success) {
nodesToAdd.push(...res.data.nodes);
linksToAdd.push(...res.data.links);
}
}
this.graph.addData(nodesToAdd, linksToAdd, idList, this.sidebarVisible);
});
};
The getNode function creates new Node objects from the data. For reference, those objects are instantiated as such
export default class Node {
id = '';
name = '';
type = '';
constructor(r) {
for (let property in r) {
// Set Attributes
this[property] = r[property];
}
}
}
anyway, the addToGraphFromIds triggers
this.graph.addData(nodesToAdd, linksToAdd);
and then we go to that function
#action
addData = (nodes, links) => {
this.structure.addNodes(nodes);
this.structure.addLinks(links);
};
which triggers
this.structure.addNodes(nodes);
which leads to this function
#observable map = new Map();
#observable map2 = new Map();
#observable dictionary = {};
#observable dictionary2 = {};
#observable allNodes = [];
#observable allLinks = [];
...
#action
addNodes = (nodes=[]) => {
if (!nodes || nodes.length === 0) return;
nodes = utils.toArray(nodes);
// Only consider each new node if it's not in the graph or a duplicate within the input list
nodes = _.uniqBy(nodes, (obj) => { return obj.id; });
const nodesToConsider = _.differenceBy(nodes, this.allNodes, (obj) => { return obj.id; });
// Add nodes to map
let currNode;
for (let i = 0; i < nodesToConsider.length; i++) {
currNode = nodesToConsider[i];
this.map.set(currNode.id, new Map());
this.map2.set(currNode.id, new Map());
this.dictionary[currNode.id] = currNode;
}
// Update internal list of nodes
this.allNodes = this.allNodes.concat(nodesToConsider);
};
As we can see in the first codebox,
this.props.appStore.repo.canvas.graph.structure.dictionary[id] !== undefined
Should cause the button to change values as we have added the current node. The nodes appear in the dictionary when I log or use mobx chrome extension dev tools, but I have to switch tabs and then the button will re-render. I've tried using other lines like
this.props.appStore.repo.canvas.graph.structure.allNodes.includes(node)
but that doesn't work either. Am absolutely stuck and need help. I have a feeling it has to do with nested observables, and maybe tagging #observable isn't good enough, but not quite sure. repo and canvas are marked as observables and instantiate a new Repo() object and new Canvas() object, much like new Node() is created in getNodes.

Mobx (v4) does not track addition or removal of entries in an observable object unless observable.map is used. Or upgrading to mobx v5 should solve the issue.
For your specific issue you can try:
#observable nodeIdToNodeData = {};
//...
this.nodeIdToNodeData = {...this.nodeIdToNodeData, [currNode.id]: currNode};
Or try to upgrade to mobx v5.
More info here

Looks like Edward solved it, similar to Redux it looks like you have to create a new object with the dictionary rather than modify it. I'm guessing it's because the key currNode.id is already defined (as the value undefined), and we're just modifying it to be currNode. That's my guess, regardless it works now.

Related

Firestore add data over an object within a document's data REACT.JS

I want to add some data on the bookChapters object, like a random id and inside of it the name of the chapters, I tried this but it doesn't work, after I add the previous data I also want to add a new object "takeAways", like the previous one, inside the random id object.
export const createNewChapter = (bookId, inputText) => {
return async dispatch => {
dispatch(createNewChapterStart());
try {
firebase
.firestore()
.doc(`Users/${bookId}/bookChapters/${inputText}`)
.onSnapshot(querySnapshot => {
//There I want to add the chapters to the firestore database
});
dispatch(createNewChapterSuccess(inputText));
} catch (error) {
dispatch(createNewChapterFail(error));
console.log(error);
}
};
};
I wanna know how to do add from scratch the bookChapters object
The database screenshot shows that the bookChapters object is a map. So to add (populate) this object you need to generate a simple JavaScript object with some properties as “key: value” pairs.
Something along these lines, making the assumption the chapter titles are in an Array:
function arrayToObject(arr) {
var obj = {};
for (var i = 0; i < arr.length; ++i) {
obj[i] = arr[i];
}
return obj;
}
const chapterList = ['Intro', 'Chapter 1', 'Chapter2', 'Conclusion'];
const bookChaptersObj = arrayToObject(chapterList);
firebase.firestore().doc(`Users/${bookId}`).update(bookChaptersObj);
Or, if the document does not already exist:
firebase.firestore().doc(`Users/${bookId}`).set(bookChaptersObj, {merge: true});

Update Set after delete using a checkbox to delete Set Item

I am using a checkbox to select items.
When the checkbox is checked, item is added to a Set stored in state.
When checkbox is unchecked, item is deleted from the Set stored in state
Upon verifying if item was removed from Set, the function
set.has(setItem)
returns false.
However when checking the size of the Set, it is as if I did not remove anything.
I'm probably not updating the set to account for the deleted item.
Can anyone help?
my handleChange function below...
const handleChange = (e) => {
if (e.target.checked) {
var checkedItemId = parseInt(e.target.value)
setCheckedItems(new Set(checkedItems.add(checkedItemId)))
}else if(!e.target.checked){
checkedItems.delete(checkedItemId)
setCheckedItems(new Set(checkedItems))
}
}
const handleChange = (e) => {
const checkedItemId = parseInt(e.target.value);
if (e.target.checked) {
setCheckedItems(prevState => {
let newSetCheckedItems = new Set(...prevState.checkedItems);
return newSetCheckedItems.add(checkedItemId);
})
} else if (!e.target.checked) {
setCheckedItems(prevState => {
let newSetCheckedItems = new Set(...prevState.checkedItems);
return newSetCheckedItems.detele(checkedItemId);
});
}
}
The solution was to use a deep copy of the set.
Do the adding/removing operations on that deep copy.
Then update the set with the deep copy.
//i don't know about the react but i can give you solution in the jquery.
var new_arr=[];
$(document.body).on('click','.class_name',function(){
var is_checked = $(this).is(':checked');
var value_check_box = $(this).val();
if(is_checked==true){
// push the value of the check box in the array variable
new_arr.push(value_check_box);
}else{
var removeItem = value_check_box // it is the value when the user uncheck the uncheckbox;
new_arr = jQuery.grep(new_arr, function(value) {
return value != removeItem;
});
};
})

React - How to separately assign click event to bunch of div's generated in loop

I am trying to create my own Game of Life in React. Currently have created a map with divs that will be separate cells in future when I finish my project. I also wanted to attach click event to each cell, but for some reason when I click single cell, the entire set of cells is affected. Can you please check why this happens? Also, can you please let me know if my approach is correct? Here is my index.js code:
class Board extends React.Component {
constructor(props){
super(props);
this.state = {isToggleOn: true};
this.changeState = this.changeState.bind(this);
}
changeState() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
createMap = (cols, total) => {
let table = []; let nL = ''; let idRow = 0; let idCol = 0;
for (let i = 0; i < total; i++) {
idCol++;
if (i%cols === 0){
nL = 'newLine';
console.log(i%cols);
idRow += 1;
idCol = 0;
}
else {
nL = '';
}
let toggledBackground = (this.state.isToggleOn ? 'test' : '');
table.push(<div id={"row-"+idRow+"-"+idCol} className={nL+" square "+toggledBackground} onClick={this.changeState}></div>);
}
return table;
}
render() {
return(
<div>
{this.createMap(COLS, FIELDS)}
</div>
);
}
}
All of them are highlighted because they all share the same state, the easiest solution would be to make a separate component for the squares and pass down the needed data as props.
This will allow you to have separate state for every single cell.
I assume FIELDS is the total number of cells (e.g for a 10x10 board, that would make FIELDS = 100). If that's the case, then you could bind the current index for each iteration to the said cell you are pushing in.
This way, you'll know which cell was clicked.
onClick={() => this.changeState(i)}
You'll also need to add a parameter to the actual function declaration, and save the state for that particular cell:
changeState(index) {
this.setState(prevState => {
let arr = prevState.cellStates.slice();
arr[index] = !arr[index];
return {cellStates: arr};
});
}
Of course, this requires you to have an array, not a single boolean:
this.state = {cellStates: Array(FIELDS).fill(false)};
and finally for your styles:
let toggledBackground = (this.state.cellStates[i] ? 'test' : '');

Is it possible to retrieve a component's instance from a React Fiber?

Before v16 of React -- that is, before the introduction of React fibers -- it was possible to take a DOM element and retrieve the React component instance as follows:
const getReactComponent = dom => {
let found = false;
const keys = Object.keys(dom);
keys.forEach(key => {
if (key.startsWith('__reactInternalInstance$')) {
const compInternals = dom[key]._currentElement;
const compWrapper = compInternals._owner;
const comp = compWrapper._instance;
found = comp;
}
});
return found || null;
};
This no longer works for React v16, which uses the new Fiber implementation. Specifically, the above code throws an error at the line const comparWrapper = compInternals._owner because there is no _owner property anymore. Thus you cannot also access the _instance.
My question here is how would we retrieve the instance from a DOM element in v16's Fiber implementation?
You may try the function below (updated to work for React <16 and 16+):
window.FindReact = function(dom) {
let key = Object.keys(dom).find(key=>key.startsWith("__reactInternalInstance$"));
let internalInstance = dom[key];
if (internalInstance == null) return null;
if (internalInstance.return) { // react 16+
return internalInstance._debugOwner
? internalInstance._debugOwner.stateNode
: internalInstance.return.stateNode;
} else { // react <16
return internalInstance._currentElement._owner._instance;
}
}
Usage:
var someElement = document.getElementById("someElement");
FindReact(someElement).setState({test1: test2});
React 17 is slightly different:
function findReact(dom) {
let key = Object.keys(dom).find(key => key.startsWith("__reactFiber$"));
let internalInstance = dom[key];
if (internalInstance == null) return "internalInstance is null: " + key;
if (internalInstance.return) { // react 16+
return internalInstance._debugOwner
? internalInstance._debugOwner.stateNode
: internalInstance.return.stateNode;
} else { // react <16
return internalInstance._currentElement._owner._instance;
}
}
the domElement in this is the tr with the data-param-name of the field you are trying to change:
var domElement = ?.querySelectorAll('tr[data-param-name="<my field name>"]')

Typescript fire event every time an array changes

I am trying to observe an array in typescript 2.3.3, and I am using rxjs. my goal is to run a method every time an array is changed. Let me clarify with some code before I can tell you what I've tried.
rows: any[] = []
rows.push('test1') //fired event now
rows.splice(0,1) //fire event now
rows = [] // fire event now
Basically, if this property ever changes then I would like to have an event called.
I have researched Rx.Observable and I came up with a couple different things.
Rx.Observable.from(this.rows) and then subscribe to it, however the subscription is never fired.
Rx.Observable.of(this.rows) and then subscribe to it, this fires the subscription only 1 time.
i think that Rx.Observable is the way to go here, but i'm not sure how to get this to fire event every time.
thank you for your advice
Looks like Proxy might fit the bill. Some preliminary testing suggests that most in-place Array mutators (like splice, sort and push) do trigger Proxy setters, so you might be able to emit values on the side as easily as:
const subj: Subject<TItem> = new Subject(); // whatever TItem is convenient for you
const rows: Array<T> = new Proxy([], {
set: function(target: Array<T>, idx: PropertyKey, v: T): boolean {
target[idx] = v;
this.subj.onNext(/* TItem-typed values here */);
return true;
}
});
Note that very nicely (and to my total total surprise), TypeScript coerces new Proxy(Array<T>, ...) to Array<T>, so all your array operations are still typed! On the downside, you will probably be flooded with events, especially handling operations that set multiple times like sort, for which you can expect O(n lg n) notifications.
I ended up creating an ObservableArray class that will fire events and has a subscribe method in it. I will post this here just so that people having this issue will be able to use it too.
export class ObservableArray {
onArrayChanges: EventEmitter<any> = new EventEmitter();
onRemovedItem: EventEmitter<any> = new EventEmitter();
onAddedItem: EventEmitter<any> = new EventEmitter();
onComponentChanges: EventEmitter<any> = new EventEmitter();
length(): number {
return this.collection.length;
}
private collectionCount: number = 0;
private isStartup:boolean=true;
constructor(public array: any[]) {
this.onComponentChanges.subscribe(data => {
this.array = data;
this.onChanges();
});
}
private collection: any[] = [];
private subscriptions: any[] = [];
onChanges(): void {
let collectionModel = new CollectionModel();
collectionModel.originalValue = this.collection;
collectionModel.newValue = (this.isStartup)?this.collection:this.array;
collectionModel.isRecordAdded = this.isAddedItem();
collectionModel.isRecordRemoved = this.isRemovedItem();
this.collectionCount = this.collection.length;
if (this.isAddedItem()) {
this.onAddedItem.emit(collectionModel);
}
if (this.isRemovedItem()) {
this.onRemovedItem.emit(collectionModel);
}
if (this.isChanged()) {
this.updateCollection();
this.onArrayChanges.emit(collectionModel);
this.publish(collectionModel);
this.isStartup = false;
}
}
private isChanged(): boolean {
let isChanged = false;
if (this.array) {
isChanged = this.array.length !== this.collectionCount;
}
return isChanged || this.isStartup;
}
private isAddedItem(): boolean {
let isAddedItem = false;
if (this.array) {
isAddedItem =this.array.length > this.collectionCount;
}
return isAddedItem;
}
private isRemovedItem(): boolean {
let isRemoved = false;
if (this.array) {
isRemoved = this.array.length < this.collectionCount;
}
return isRemoved;
}
private updateCollection() {
this.collection = this.array;
}
private publish(payload?: any) {
for (let callback of this.subscriptions) {
callback(payload);
}
}
subscribe(callback: (payload?: any) => void) {
this.subscriptions.push(callback);
}
}
export class CollectionModel {
originalValue: any[] = [];
newValue: any[] = [];
isRecordAdded: boolean = false;
isRecordRemoved: boolean = false;
}
If you really wanted you could make this and ObservableArray<T> however in my case i didn't think that i needed to do this.
I hope this helps others. :)
try use "spread" instead of "push"
rows = [...rows,"test"]

Resources