Update FireStore Document on Read - reactjs

I am using React and Firestore to make a Blog like site. Each Post is Stored as a separate document in Post collection.
The Document structure looks like :
--Posts
--Post1
--title
--body
--views
--likes
--Post2
--title
--body
--views
--likes
I want to update the views whenever someone Views a post. What is the best way to Achieve this in Firestore.

For many cases you would use update directly. However since you're incrementing a counter you should also use a transaction to prevent race conditions when multiple users are trying to increment it around the same time:
// Create a reference to the post.
const postRef = db.collection('Posts').doc('<postId>');
return db.runTransaction(function(transaction) {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(postRef).then(function(post) {
if (!post.exists) {
throw new Error('Post does not exist!');
}
const newViews = post.data().views + 1;
transaction.update(postRef, { views: newViews });
});
}).then(function() {
console.log('Transaction successfully committed!');
}).catch(function(error) {
console.log('Transaction failed: ', error);
});

Related

Firebase data deleted upon page refresh in React

I have been stumped on a work around for this problem for a while now and was hoping someone could help.
I am currently working on a React UI that sends info to the backend Firebase for a budgeting app.
When the page first loads, I pull in the data using this:
const [incomeSources, setIncomeSources] = React.useState([]);
/////////////////////////////////
// PULL IN DATA FROM FIREBASE //
///////////////////////////////
async function getData() {
const doc = await getDoc(userCollectionRef);
const incomesData = doc.data().incomeSources;
// const expensesData = doc.data().expenses;
// const savingsData = doc.data().savingsAllocation;
// SET STATES //
if (incomesData.length > 0) {
setIncomeSources(incomesData);
}
}
then when I want to add a new object to the state array I use a input and button. The issue I currently have is that I have it set up like this:
async function updateFirebaseDocs(userID, stateName, state) {
const userRef = doc(db, "users", userID);
try {
await setDoc(userRef, { [stateName]: state }, { merge: true });
} catch (err) {
console.error("error adding document", err);
}
}
React.useEffect(() => {
updateFirebaseDocs(userID, 'incomeSources', incomeSources)
},[incomeSources])
this works so long as I don't refresh the page, because upon page refresh, incomeSources defaults back to an empty array on render. Causing firebase docs to become an empty array again which deletes firestore data.
I can't for the life of me figure out the workaround even though I know its probably right in front of me. Can someone point me in the right direction please.
Brief summary: I am able to pull in data from backend and display it, but I need a way to keep the backend database up to date with changes made in the Frontend. And upon refreshing the page, I need the data to persist so that the backend doesn't get reset.
Please advise if more information is needed. First time posting.
I have tried using the above method using useEffects dependency, I have also tried using localstorage to work around this but also don't can't think of a way of implementing it. I feel I am tiptoeing around the solution.

How to force Apollo Client to use cached data for detail view page

I have a paginated cursor based query TODOS and detail page with query TODO to get data by ID.
Whenever I go to detail view and use useQuery with TODO query (Which contains exactly same data as TODOS query result, it still tries to get data from server not from cache. How can I achieve to not get data from server (Because it already exists), I thought Apollo detect by id and return from the cache but no. Any suggestions ?
Similar issue as no this post, but I don't think thats a right approach, there should be better solution. (I hope)
This is TODOS query:
query TODOS(
$paginationOptions: PaginationOptionsInput
) {
todos(paginationOptions: $paginationOptions) {
pagination {
minCursor
maxCursor
sortOrder
limit
hasMoreResults
}
result {
id
...SomeTodoFields
}
}
And on detail page I have second query TODO
query (
$todoId: String!
) {
todo(todoId: $todoId) {
id
...SomeTodoFields
}
}
Since I am using Apollo-client < 3.0 for me cacheRedirect worked fine, you can have a look farther here. Read every note carefully it is really important! My code example:
cache: new InMemoryCache({
fragmentMatcher,
cacheRedirects: {
Query: {
todo: (_, args, { getCacheKey }) => {
return getCacheKey({ __typename: 'TodoType', id: args.todoId })
}
}
}
})
})
Found some good relevant article as well, which you might want to check.
This worked for me, hope it helps to someone else as well. :)

How do I sort a list of users by name on a node server?

I have created several user accounts on mongodb and i want to sort them out by user name. I compare the user names in the database against a string provided through aaxios request with a body value that is taken from an input value, like this:
frontend
const findUsers = async () => {
try {
const response = await axios.post(`http://localhost:8080/search-users/${_id}`, { searchValue });
setReturnedUser(response.data.matchedUser);
} catch (error) {
console.log(error);
}
}
findUsers();
backend
exports.sort = (req, res) => {
let result;
User.find({ name: req.body.searchValue }).exec((error, users) => {
if (error) {
return res.status(400).json({
message: error,
});
}
result = users;
res.status(200).json({
message: 'Description added successfully',
matchedUser: result,
});
});
};
The problem with this approach is that the users are returned only after I type in the entire name.
What I want is that the users to get returned as I type in the name, so several matching users will het returned when I start typing and as I continue the list will narrow down, until only the matching user remains.
I have successfully achieved this on the react side, but that was possible only by fetching all the users from the database, which would be a very bad idea with a lot of users. The obvious solution is to do the sorting on the server.
Filtering on the client-side is possible but with some tweaks to your architecture:
Create an end-point in node that returns all the users as JSON. Add caching onto this end-point. A call to origin would only occur very infrequently. YOu can then filter the list easily.
Use something like GraphQL and Appollo within node. This will help performance
To do the filtering in node you can use a normal array.filter()
I woul do the filter in mongo as the quick approach and then change it if you notice performance issues. It is better no to do pre-optimisation. As Mongo is NoSQL it wshould be quick

React Native [iOS] Database Options

I am looking to create an iOS app in React Native and wondering what's the best database option to fit my app.
The app will be supported by ~300 data objects which will be fetched as json objects from a remote server. There are some variances in attributes across these 300 objects. Therefore, I am hesitant to set up an inflexible database schema.
Ideally, I would just like 1 database with 2 attributes:
1) id (for example: from 1 to 300)
2) data (for example: {"hello" : "world"})
Give the above, any suggestions on what kind of react-native database module I should use?
Thanks
According to my own experience in previous successful react-native project, you could use AsyncStorage, which is simple but yet powerful enough, you can store whatever you want!
Besides, you should also use flux or redux, which will provide you a Store solution where you can read & store the data related to AsyncStorage easily (everywhere, on every page)!
The steps are (main flow's idea on how to organise your data and structure the logics):
0/ Importing libraries:
import React, { Component } from 'react';
import {
AsyncStorage,
// ...
} from 'react-native';
1/ Get data from your API or somewhere (local files etc.), then write (save) the data to AsyncStorage:
async firstWriteFavorite() {
fetch("YOUR_API_URL", {method: "GET", headers: {'Cache-Control': 'no-cache'}})
.then((response) => response.json())
.then((responseJson) => {
try {
// make sure you write a STRING into AsyncStorage,
// and also be careful which key of the JSON should be written, the below line is just a good example:
await AsyncStorage.setItem("#PROJECT_NAME:your_favorite_array", JSON.stringify(responseJson.response));
// if you use flux or redux here, you can perform some action here, then you can load the data everywhere later:
// FavoriteActionCreators.set_favorite(responseJson.response);
} catch (error) {
console.log('AsyncStorage error: ' + error.message);
}
})
.catch((error) => {
console.log("Error in first getting ajax data!", error);
}
);
}
2/ Retrieve the data from AsyncStorage:
async loadFavorite() {
try {
var fav_array_string = await AsyncStorage.getItem("#PROJECT_NAME:your_favorite_array");
// the above returned value is a STRING, then you can split it, or do whatever based on the structure you have written
var real_fav_id_array = fav_array_string.split('YOUR_SEPARATOR');
// ...
} catch (error) {
console.log('AsyncStorage error: ' + error.message);
}
}
3/ When you need to update the data, firstly retrieve data, then bind the data to a variable and make changes to that variable, next write the new data to AsyncStorage:
async saveFavorite() {
// loadFavorite() here,
// make sure you've got data "your_new_JSON_data" which has been converted into object, then maybe: "your_new_JSON_data.push({NEW_OBJ})";
// after that, SAVE NEW DATA now:
try {
await AsyncStorage.setItem("#PROJECT_NAME:your_favorite_array", JSON.stringify(your_new_JSON_data));
// same here, if you use flux or redux here, you can save the new data here:
// FavoriteActionCreators.set_favorite(your_new_JSON_data);
} catch (error) {
console.log('AsyncStorage error: ' + error.message);
}
}
The above code was copied and shortened from my real project's code, please try and tell me if you have any problem!

Creating and adding to arrays in angularFire / Firebase

I am trying to build an app with angularjs and Firebase similar to a forum for helping my classmates with problems. I also want people to be able to 'reply' to the specific problems the classmates are having, so I create an object with many values in the angularjs factory, like this:
factory.addError = function() {
factory.errors.$add({
...
replies: []
});
};
The problem with this is that Firebase doesn't save parameters with empty values for placeholders, such as the parameter 'replies' above. I have tried to hard code a placeholder value into the array, but that seems like a very patchy solution, and that comes with it's own set of problems for me having to delete out the data in Firebase. For reference, here is the code in the linked controller:
$scope.error.replies.$push({
name: $scope.replyName,
message: $scope.replyMessage,
time: (new Date()).toString(),
upvote: 0
});
How do you initialize an empty array into the object? And will $push properly use Firebase's commands to save it to it's own set of data?
First, here are some relevant resources and suggestions:
Resources
Check out Firebase's blog post, Best Practices: Arrays in Firebase - "Arrays are evil".
Also, the AngularFire Development Guide.
And the documentation for AngularJS providers.
Suggestions
As the AngularFire API Documentation says:
"There are several powerful techniques for transforming the data downloaded and saved by $firebaseArray and $firebaseObject. These techniques should only be attempted by advanced Angular users who know their way around the code."
Putting all that together, you accomplish what you want to do by:
Extending AngularFire services, $firebaseArray and $firebaseObject.
Following the documentation for extending services.
Example
Extended Error $firebaseObject
.factory('Error', function(fbUrl, ErrorFactory) {
return function(errorKey){
var errorRef;
if(errorKey){
// Get/set Error by key
errorRef = new Firebase(fbUrl + '/errors/'+errorKey);
} else {
// Create a new Error
var errorsListRef = new Firebase(fbUrl + '/errors');
errorRef = errorsListRef.push();
}
return new ErrorFactory(errorRef);
}
})
.factory('ErrorFactory', function($firebaseObject){
return $firebaseObject.$extend({
sendReply: function(replyObject) {
if(replyObject.message.isNotEmpty()) {
this.$ref().child('replies').push(replyObject);
} else {
alert("You need to enter a message.");
}
}
});
})
Error Controller
.controller('ErrorController',function($scope, Error) {
// Set empty reply message
$scope.replyMessage = '';
// Create a new Error $firebaseObject
var error = new Error();
$scope.error = error;
// Send reply to error
$scope.reply = function(){
error.sendReply({message:$scope.replyMessage});
}
})
And String.prototype.isNotEmpty()
String.prototype.isNotEmpty = function() {
return (this.length !== 0 && this.trim());
};
(adapted from this answer)
Hope that helps!

Resources