I've searched and I've searched and I've searched, I can't find an answer to this.
I am using Angular Firestore to fetch data (tried both snaphotChanges and valueChanges). The code works.
However, if the data changes on the backend, the material table row on the UI only refreshes automatically when either I am showing all the data or if I disable paging. However, when paging is enabled, the UI resorts to show that the data got updated, but it does not refresh the cell value until I move the cursor on the row to force a refresh. I don't know if it's a bug or if I am doing something wrong.
I made a video showing what happens: https://youtu.be/SN6EFc6Vqm8
// Have this in my service. It works
getContactsByLimit(limit) {
return this.afs
.collection("users")
.doc(this.user.currentUserId)
.collection("contacts", ref =>
ref
.orderBy("id", "desc")
.limit(limit)
)
.valueChanges();
}
// The paginator declaration
#ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
// This works
this.contactsSubscription = this.contactsvc
.getContactsSnapshotChangesByLimit(3)
.subscribe(contacts => {
// This works, I get new data.
this.dataSource.data = contacts;
});
// HTML template
<mat-paginator class="paginator"
[length]="15" // This breaks the refresh
[pageSize]="3"
[pageSizeOptions]="[2,3,5]"
(page)="pageEvent($event)"></mat-paginator>
// The automatic refresh does not work if I set
// the length to be > 3 in this case (unless I move
// the mouse on the table).
The UI should refresh with new data whether the length > pageSize or not.
Figured it out. I was using trackBy to track by ID. I added a timestamp field in my document and started tracking by timestamp.
<table [dataSource]="dataSource" [trackBy]="trackByTimestmamp" mat-table>
and
trackByTimestmamp(index, item) {
return item.timestamp;
}
This forces the material table to update the table when the timestamp changes.
Related
In a chat app I am building I want to deduct credits from a user's account, whenever the users sends a message and when a chat is initiated.
The user account is accessible in the app as a context and uses a snapshot listener on a firestore document to update whenever something changes in the user account document. (See code samples 1. and 2. at the bottom)
Now whenever anything in the userAccount object changes, all of the context providers children (NavigationStructure and all its subcomponents) are re-rendered as per React's documentation.
This, however causes huge problems on the chat screen that also uses this context:
The states that are defined there get re-initalized whenever something in the context changes. For example, I have a flag that indicates whether a modal is visible, default value is visible. When I go onto the chat screen, hide the modal, change a value manually in the firestore database (e.g. deduct credits) the chat screen is rerendered and the modal is visible again. (See code sample 3.)
I am very lost what the best way to solve this issue is, any ideas?
Solutions that I have thought about:
Move the credits counter to a different firestore document and deduct the credits once per day, but that feels like a weird workaround.
From Googling it seems to be possible to do something with useCallback or React.memo, but I am very unsure how.
Give up and become a wood worker...seems like running away from the problem though.
Maybe it has something to the nested react-navigation stack and tab navigators I'm using within NavigationStructure?
Desperate things I have tried:
Wrap all sub-components of NavigationStructrue in "React.memo(..)"
Make sure I don't define a component within another component's body.
Look at loads of stack overflow posts and try to fix things, none have worked.
Code Samples
App setup with context
function App() {
const userData = useUserData();
...
return (
<>
<UserContext.Provider value={{ ...userData }}>
<NavigationStructure />
</UserContext.Provider>
</>
}
useUserData Hook with firestore snapshot listener
export const useUserData = () => {
const [user, loading] = useAuthState(authFB);
const [userAccount, setUserAccount] = useState<userAccount | null>();
const [userLoading, setUserLoading] = useState(true);
useEffect(() => {
...
unsubscribe = onSnapshot(
doc(getFirestore(), firebaseCollection.userAccount, user.uid),
(doc) => {
if (doc.exists()) {
const data = doc.data() as userAccount & firebaseRequirement;
//STACK OVERFLOW COMMENT: data CONTAINES 'credits' FIELD
...
setUserAccount(data);
}
...
}
);
}, [user, loading]);
...
return {
user,
userAccount,
userLoading: userLoading || loading,
};
};
Code Sample: Chat screen with modal
export const Chat = ({ route, navigation }: ChatScreenProps): JSX.Element => {
const ctx = useContext(UserContext);
const userAccount = ctx.userAccount as userAccount;
...
//modal visibility
const [modalVisible, setModalVisible] = useState(true);
// STACK OVERFLOW COMMENT: ISSUE IS HERE.
// FOR SOME REASON THIS STATE GET'S RE-INITALIZED (AS true) WHENEVER
// SOMETHING IN THE userAccount CHANGES.
...
return (
<>
...
<Modal
title={t(tPrefix, 'tasklistModal.title')}
visible={ModalVisible}
onClose={() => setModalVisible(false)}
footer={
...
}
>
....
</Modal>
...
</>)
}
Any change to the context does indeed rerender all consumer components whether they use the changed property or not.
But it will not unmount and mount the component which is the reason why your local state gets initialized to the default value.
So the problem is not the in the rerenders (rarely the case) but rather <Chat ... /> or one of it's parent component unmounting due to changes in the context.
It is hard to tell from the partial code examples given but I would suggest looking at how you use loading. Something like loading ? <div>loading..</div> : <Chat ... /> would cause this behaviour.
As an example here is a codesandbox which illustrates the points made.
This is a characteristic of React Context - any change in value to a context results in a re-render in all of the context's consumers. This is briefly touched on in the Caveats section in their docs, but is expanded on in third-party blogs like this one: How to destroy your app's performance with React Context.
You've already tried the author's suggestion of memoization. Memoizing your components won't prevent re-initialization, since the values in the component do change when you change your user object.
The solution is to use a third-party state management solution that relies not on Context but on its own diffing. Redux, Zustand, and other popular libraries do their own comparison so that only affected components re-render.
Context is really only recommended for values that change infrequently and would require full-app re-renders anyway, like theme changes or language selection. Try replacing it with a "real" state management solution instead.
I am grabbing some data from Firebase to display on my webpage. While the text is loaded correctly, I keep getting errors in my console saying that I am loading undefined variables. This is important because I eventually want to be able to add an edit feature. I determined that the Firebase call is rendering after the DOM is loaded and that the data is set to null initially.
I've tried to use different lifecycle hooks but none of them wait till the Firebase call is complete. I am not that experienced with Javascript so I may be missing something simple.
created(){
db.collection('recipes').doc("Grilled Cheese").get()
.then((doc) => {
this.contentData = doc.data();
})
}
data() {
return {
sidebarData: null,
contentData: "",
}
},
I want to populate the contentData with the correct values before the DOM renders completely.
Knowing that you can't force the mounted hooks to be executed after the created hook ends ( the DOM render will not wait for your firebase response ) ... you need to add a conditional v-if on your template so only if the data is available ( not undefined ) the DOM will be rendered.
<template v-if="contentData">...</template>
NB : null and "" are defined data and that should not cause any rendering issues ... make sure doc.data() is not undefined
How to update a single object within node.
{
foo:
{
title: 'hello world',
time: '1000'
}
}
As above, I just want update title.
$firebase(new Firebase(ref).child('foo')).$save(); will update the entire node. Also tried $save('title') but not work.
The reason I just want to update a single object, because some of the ng-model doesn't need to update to firebase.
Heres an example setting the title to "whatever"
$firebase(new Firebase(ref)).$child('foo').$child('title').$set("whatever")
I have recently been working with angularfire and firebase. I am not sure if this is a fix but i dont think you dont need to explicitly select the item property you want to update when you are using the $save() method.
For instance, in my use case i was selecting a user and had an init function that got that whole object
html
<ul ng-repeat="user in vm.users>
<button ng-click="vm.updateUserInit(user)">Edit</button>
</ul>
This then opened up the update form with the users properties. In the controller i took that user and assigned it into a $firebaseObject.
controller
var selectedUser;
vm.updateUserInit = function (user) {
var ref = new Firebase("https://<foo>.firebaseio.com/users/" + user.$id);
selectedUser = $firebaseObject(ref);
}
It justs puts the user into a firebase object as the variable selectedUser to use in the $save.
Then when the user updates the details
html
<button type="button" ng-click="vm.updateUserDetails()">Update User Details</button>
the function already has the selected user as an object to use and anything added will be updated. Anything omitted will not.
controller
vm.updateUserDetails = function () {
selectedUser.firstName = vm.firstName;
selectedUser.$save();
}
The selectedUser.$save(); changes only the first name even though the user has 6 other properties
Not sure if this helps, trying to wrap my head around all of this as well. Check out valcon if you dont already have it. Really nice extension on the chrome inspector for firebase
UPDATE FOR ANGULARFIRE 2
if you create a service for your calls , which could serve all update calls
constructor(private db: AngularFireDatabase) {}
update(fbPath: string, data: any) {
return this.db.object(fbPath).update(data);
}
And then in the page component
this.api.update(`foo`, ({ title: 'foo Title' }) );
Much simpler
Meteor newbie here. Working off of the Todos example, I am trying to use a separate Tags collection. It seems to be working except for a strange UI artifact: If I click a tag in the tag filter, and check off the last item in the todo list, the first item gets checked as well. The first item does not get updated to done, and clicking away from the tag filter and then back shows the first item unchecked as it should be. So I am not sure why that is happening.
The code for the todos is the same as in the Todos example
{{#each todos}}
{{> todo_item}}
{{/each}}
And the code for the tags collection filter
var todos = [];
if (!currentTaskId)
return {};
var tag_filter = Session.get('tag_filter');
if (tag_filter){
var tags = Tags.find({taskId: currentTaskId, name: tag_filter});
tags.forEach(function(tag){
var todo = Todos.findOne(tag.todoId);
todos.push(todo);
});
return todos; // this is an array rather than a collection and causes a strange artifact bug when checking bottom todo as done
}
What I have been able to gather is that if you do {{#each}} on an array you create a dependency on the entire scope rather than each individual item in the array, versus a collection cursor that automagically creates a dependency for each document in the collection. Has anybody run into this odd UI behavior? I'd also like to know if there is a way to either make the array into a cursor or at least act like one by registering a dependency for each item in the array?
Appreciate any insights, thank you.
I've revamped your code to return a cursor instead of an array, it may solve your problem but it's untested.
var tagFilter=Session.get("tag_filter");
if(!currentTaskId || !tagFilter){
return null;
}
// find tags and fetch them in an array
var tags=Tags.find({
taskId:currentTaskId,
name:tagFilter
}).fetch();
// build an array of Todos ids by extracting the todoId property from tags
// see underscore docs
var todosIds=_.pluck(tags,"todoId");
// return todos whose id is contained in the array
return Todos.find({
_id:{
$in:todosIds
}
});
I'm very new to meteor, so apologies if I'm missing something very basic here.
I thought it would be fun to create a very simple textpad style app to check out meteor. I took the todo app and changed the data structures to be 'folders' and 'docs' rather than 'lists' and 'todos', so I have a list of folders and when you click on the folder you get a list of the documents in that folder.
I've then added some code to show the 'content' attribute of a single 'doc' when one of the docs in the list is clicked.
I'm using ace to add some pretty print to the content of the doc (https://github.com/ajaxorg/ace). I've set ace up to work with a hidden textarea containing the plaintext version of my document, and the editor object takes this text and pretty prints it.
The problem with ace is that I don't want the template containing the ace editor to be replaced every time the contents of the doc changes (as it takes half a second to reinitialise, which is a crappy experience after every character is typed!). Instead, I want to update the textarea template and then use the ace API to tell the editor to update it's input based on what is in the textarea.
Now, this is probably the wrong way to approach the problem, but I've ended up using two templates. The first contains a textarea containing doc.contents, which is reactive to the underlying model:
<template name="doc_content">
<textarea name="editor">{{content}}</textarea>
</template>
The second one contains the 'editor' div which ace uses to display the pretty printed text.
<template name="doc_init">
<div id="editor"></div>
</template>
The idea is that the first template will update every time the user types (on all clients), and the second template is only ever re-loaded for each new doc we load.
Template.doc_content.content = function() {
var doc_id = Session.get('viewing_itemname');
if (!doc_id) {
return {};
}
var doc = Docs.findOne({_id:doc_id});
if (doc && doc.content) {
// #1 Later
var editor = Session.get('editor');
if (editor) {
editor.getSession().setValue(doc.content);
}
return doc.content;
} else {
return '';
}
};
When you enter text into the editor div I make a call to Docs.update(doc_id, {$set: {content: text}});, which updates the value in the textarea on each client. All good so far.
editor.getSession().on('change', function(){
var text = editor.getSession().getValue();
Docs.update(doc_id, {$set: {content: text}});
});
What I want to do, for all clients other than the client which made the change, is to subscribe to the change for that doc and call editor.getSession().setContent() with the text which has just been changed, taking the text from the textarea and using it to fill the editor.
I've tried to do this by making that call from the template containing the textarea (as this changes whenever the doc is updated - see #1 above). However, this puts the clients into an infinite loop because changing the value in the editor causes another call to Docs.update.
Obviously this doesn't happen when you render a template, so I'm assuming there's some magic in meteor which can prevent this happening, but I'm not sure how.
Any thoughts?
TIA!
There's a lot to absorb in your question, but if I understand correctly, you might simply be after Deps.autorun:
Deps.autorun(function () {
var doc_id = Session.get('viewing_itemname');
if (!doc_id) {
return {};
}
var doc = Docs.findOne({_id:doc_id});
// do stuff with doc
});
Deps.autorun is really useful in that it will get re-run if any of its
dependencies change. These dependencies are limited to those that are "reactive"
such as Collections and Sessions, or anything that implements the reactive API.
In your case, both Session.get and findOne are reactive so if their values
change at all, Deps.autorun will run the function again.