Firebase does not save empty array - reactjs

Hello I have a question I am making app with Firebase and React. The problem is that I want to have comment array but it is empty when the item is created. How can I solve this problem and have an empty array and then update this array after adding comments.
There is a lot of code.
const onSubmitHandler = (e: React.FormEvent): void => {
e.preventDefault();
if (user?.displayName) {
const recipe: Recipe = {
username: user.displayName,
title: title,
type: type,
description: description,
id: Math.random(),
time: time,
ingredients: ingredients,
stars: Math.floor(Math.random() * 6) + 1,
steps: steps,
comments: [],
};
dispatch(recipeAction.addRecipe(recipe));
dispatch(sendData(recipe));
navigate("/");
}
};
Redux action
export const fetchRecipes = () => {
return async (dispatch: ThunkDispatch<{}, {}, AnyAction>) => {
dispatch(uiAction.isLoading(true));
const getRecipes = async () => {
const response = await fetch(
*FIREBASE API*
);
const data = response.json();
return data;
};
try {
const data = await getRecipes();
console.log(data);
if (data) {
for (const key of Object.keys(data)) {
dispatch(recipeAction.replaceRecipes(data[key]));
}... other not needed code.
Redux slice
replaceRecipes(state, action: PayloadAction<Recipe>) {
const fetchedRecipe = action.payload;
state.recipes.push(fetchedRecipe);
},

Why won't it save an empty array?
I don't have so much knowledge so I am just explaining it in Java.
When you create a new empty array like this:
List array = new ArrayList<>();
its size is 0. And, 0 can in other words be said null for an empty array. Now, Firebase ignores null or 0 sized array as it had nothing to display in the console.
Then what should I do?
It is not a hard thing to do that. Now, in your case, because there is no such key in the database, it will give null and then you try using it you'll get an exception. This is what every developer wants to prevent. So the solution can be to store a boolean in the database with the other keys to check if a data has the array or not. When the post is just created, there are no comments. You can set it to false. Now, when a user adds a comment, set it to true.
Now when fetching the comments, first check if the boolean is true or not. If true, fetch the comments. Or else just ignore.
Code?
As mentioned, I have not used React.JS so much so can't provide the code. But I hope that you will be able to do it as you have already done so much of it by yourself.

Related

Const is not defined -> Yet standard solutions don't work

I want to display a mapped list where "UserName" is an entry value from a Firebase Realtime Database corresponding to the author of each entry.
The following code, inside the get(UsernameRef).then((snapshot) =>{}) scope, returns an undefined reference error as expected, 'UserName' is assigned a value but never used and 'UserName' is not defined
const [RecipeLibrary, setRecipeLibrary] = React.useState([]);
React.useEffect(() => {
const RecipeLibraryRef = ref(db, "Recipes/");
onValue(RecipeLibraryRef, (snapshot) => {
const RecipeLibrary = [];
snapshot.forEach((child) => {
const AuthorUserId = child.key;
child.forEach((grandChild) => {
const UserNameRef = ref(db, "Account/" + AuthorUserId + "/username");
get(UserNameRef).then((snapshot) => {
const UserName = snapshot.val();
});
RecipeLibrary.push({
name: grandChild.key,
author: UserName,
...grandChild.val(),
});
});
});
setRecipeLibrary(RecipeLibrary);
console.log({ RecipeLibrary });
});
}, []);
I've tried:
Using a React state to pass the variable -> Can't use inside React useEffect
Exporting and Importing a separate function that returns the desired UserName -> return can only be used in the inner scope
Moving the list .push inside the Firebase get scope -> React.useState can no longer access the list
I'm hoping there is a simple solution here, as I am new.
Your time and suggestions would mean a lot, thank you!
Update:
I got the RecipeLibrary array to contain the desired "UserName" entry, named author by moving the array .push inside the .then scope. Here is a log of that array at set (line 59) and at re-render (line 104).
child.forEach((grandChild) => {
const UserNameRef = ref(db, "Account/" + AuthorUserId + "/username");
get(UserNameRef).then((snapshot) => {
const UserName = snapshot.val();
RecipeLibrary.push({
name: grandChild.key,
author: UserName,
authorId: AuthorUserId,
...grandChild.val(),
});
});
});
});
setRecipeLibrary(RecipeLibrary);
console.log(RecipeLibrary);
However, now the mapped list is not rendering at all on screen.
Just some added context with minimal changes to original code, been stuck on this so long that I'm considering a full re-write at this point to jog my memory. Oh and here is the bit that renders the mapped list in case:
<Box width="75%" maxHeight="82vh" overflow="auto">
{RecipeLibrary.map((RecipeLibrary) => (
<Paper
key={RecipeLibrary.name}
elevation={3}
sx={{
etc...
This is a tricky one - the plainest option might be to move push() and setRecipeLibrary() inside the then() callback so they're all within the same scope, but that would have some terrible side effects (for example, triggering a re-render for every recipe retrieved).
The goal (which you've done your best to achieve) should be to wait for all the recipes to be loaded first, and then use setRecipeLibrary() to set the full list to the state. Assuming that get() returns a Promise, one way to do this is with await in an async function:
const [RecipeLibrary, setRecipeLibrary] = React.useState([]);
React.useEffect(() => {
const RecipeLibraryRef = ref(db, "Recipes/");
onValue(RecipeLibraryRef, (snapshot) => {
// An async function can't directly be passed to useEffect(), and
// probably can't be accepted by onValue() without modification,
// so we have to define/call it internally.
const loadRecipes = async () => {
const RecipeLibrary = [];
// We can't use an async function directly in forEach, so
// we instead map() the results into a series of Promises
// and await them all.
await Promise.all(snapshot.docs.map(async (child) => {
const AuthorUserId = child.key;
// Moved out of the grandChild loop, because it never changes for a child
const UserNameRef = ref(db, "Account/" + AuthorUserId + "/username");
// Here's another key part, we await the Promise instead of using .then()
const userNameSnapshot = await get(UserNameRef);
const UserName = userNameSnapshot.val();
child.forEach((grandChild) => {
RecipeLibrary.push({
name: grandChild.key,
author: UserName,
...grandChild.val(),
});
});
}));
setRecipeLibrary(RecipeLibrary);
console.log({ RecipeLibrary });
};
loadRecipes();
});
}, []);
Keep in mind that Promise.all() isn't strictly necessary here. If its usage makes this less readable to you, you could instead execute the grandChild processing in a plain for loop (not a forEach), allowing you to use await without mapping the results since it wouldn't be in a callback function.
If snapshot.docs isn't available but you can still use snapshot.forEach(), then you can convert the Firebase object to an Array similar to Convert A Firebase Database Snapshot/Collection To An Array In Javascript:
// [...]
// Change this line to convert snapshot
// await Promise.all(snapshot.docs.map(async (child) => {
await Promise.all(snapshotToSnapshotArray(snapshot).map(async (child) => {
// [...]
// Define this somewhere visible
function snapshotToSnapshotArray(snapshot) {
var returnArr = [];
snapshot.forEach(function(childSnapshot) {
returnArr.push(childSnapshot);
});
return returnArr;
}
Note that if get() somehow doesn't return a Promise...I fear the solution will be something less straightforward.

Async function runs continuously

I'm using the following code to fetch data from two different sources in react using hooks.
const [ permissionTree, setPermissionTree ] = useState([]);
const [ availablePermissionsInRole, setAvailablePermissionsInRole ] = useState<Permission[]>([]);
const getAllPermissions = (): void => {
getPermissionList()
.then(response => {
if (response.status === 200 && response.data && response.data instanceof Array) {
const permissionStringArray = response.data;
let permissionTree: Permission[] = [];
permissionTree = permissionStringArray.reduce((arr, path) => addPath(
path, path.resourcePath.replace(/^\/|\/$/g, "").split('/'), arr,
), []);
setPermissionTree(permissionTree);
}
})
.catch(error => {
//Handle Permission Retrieval Properly
})
}
/**
* Retrieve permissions for a given role if in Role edit mode.
*/
useEffect(() => {
if (isEdit && roleObject) {
getPermissionsForRole(roleObject.id)
.then(response => {
if (response.status === 200 && response.data instanceof Array) {
const permissionsArray: Permission[] = [];
response.data.forEach(permission => {
permissionsArray.push({
id: permission,
isChecked: false,
fullPath: permission
})
})
setAvailablePermissionsInRole(permissionsArray);
getAllPermissions();
}
})
.catch(error => {
//Handle Role Retrieval Properly
})
} else {
getAllPermissions();
}
}, [])
The first async call getPermissionsForRole returns a string array and the second getAllPermissions returns an array of objects which I then parse on to a util method to create a different array of objects.
With an empty array as the second argument in useEffect the continuous async call is stopped but when I check availablePermissionsInRole inside the getAllPermissions method, it's empty. When I pass availablePermissionsInRole as the second argument the continuous loop occurs.
What am I doing wrong in this code. Please guide me since I'm new to react hooks.
I found a solution for the infinite loop of the useEffect and since I pass the availablePermissionsInRole as the second argument, react look only at the array reference and treats it as a new array hence the infinite loop. As a fix for this I first passed the following as the argument.
availablePermissionsInRole.length
which will stay the same unless the backend sends a new array element. Then again the strings inside the array could change without the length being changed so I used the following as the arguement which fixes my issue.
availablePermissionsInRole.toString()
Which then changes if a new string is also retrieved.
Thank you all for the help but do point out if there is any errors in the approach I have used.

Lookup multiple Firebase entries using Redux action

I have this Redux action, which looks up a user and returns all the items they have:
export const itemsFetch = () => {
const { currentUser } = firebase.auth();
return dispatch => {
firebase
.database()
.ref(`users/${currentUser.uid}/items`)
.on('value', snapshot => {
dispatch({ type: ITEMS_FETCH_SUCCESS, payload: snapshot.val() });
});
};
};
Works great, and each item returned has a unique key associated with it.
I want to modify this action to look up specific items, which I've done. That works fine too:
export const itemLookup = uid => {
const { currentUser } = firebase.auth();
return dispatch => {
firebase
.database()
.ref(`users/${currentUser.uid}/items/${uid}`)
.on('value', snapshot => {
dispatch({ type: ITEM_LOOKUP_SUCCESS, payload: snapshot.val() });
});
};
};
This also works fine, but I can only use this to lookup a single item.
I want to loop over an array of item ids, and lookup details for each. Doing this from a component, and using mapStateToProps, causes the component to rerender each time, losing the previous lookup in the process.
Is it best to loop over the ids I have at a component level, and make multiple calls. Or should I pass the array to the action, and somehow loop over them within the action?
Thanks
I feel like I'm doing something dumb, or misunderstanding Redux completely.
In my opinion, this is one of the few limitations that firebase has (along side with queries) that sometimes make me want to grow hair again and lose it (I am bald).
I am more experienced with Firestore although I have used Database, but I think you are correct that you can only request one item in Firebase. What I would do to solve this, is to create a new action that receives an array of IDs and then executes and array of promises that will query each doc.
Something like (pseudo code, and you might need to wrap your firebase call into a promise):
let promises = [];
arrayIds.forEach(id => {
promises.push(firebase.database().ref.get(id))
})
return Promise.all(promises).then(dispatch(results))
Now, if you find that the amount of results are usually not a lot, it is totally fine (and usually the way Firebase requires you to) to complete the data filtering in the client.
Using the response from sfratini, I managed to work it out. Here's the action:
export const itemLookup = oid => {
const { currentUser } = firebase.auth();
const items = []
return dispatch => {
new Promise(function (res, rej) {
oid.forEach(id => {
firebase.database().ref(`users/${currentUser.uid}/items/${id}`).on('value', snapshot => {
items.push(snapshot.val())
})
})
}).then(dispatch({ type: ITEM_LOOKUP_SUCCESS, payload: items }))
}
I used the same reducer, and now the items array makes it down to component level. Perfect!

What's the best way to check if an element exists in a firestore array, and if it does, run a function, and if not add it?

I'm developing a 'like' button and currently the likes increment up by 1 each time the like button is clicked. However, I want to make this more robust and dynamic, and if a logged in user has already liked a post, then I want their 'like' to go away ala Facebook, Reddit (upvote), etc.
Currently what I'm doing is keeping track of the number of likes and who has liked a post. The structure looks like this:
post: {
likes: 3,
likedBy: ['userA', 'userB', 'userC']
}
So what I want to happen is: when the like button is clicked, I want to search the likedBy property to see if the logged in user has already liked the post, and then either increment liked by 1, and add them to the array, or decrement likes by 1 and remove them from the array.
I'm having a hard time figuring out how to write this logic with the React action that handles this interaction with firestore.
Here is what I have written so far:
export const newLike = (post) => {
return (dispatch, getState, {getFirebase, getFirestore}) => {
const firestore = getFirestore();
const signedInUser = getState().firebase.auth;
console.log(signedInUser)
firestore.collection('posts').doc(post.id).update({
likes: (post.likes + 1),
likedBy: firestore.FieldValue.arrayUnion(signedInUser)
}).then(()=> {
dispatch({type: types.NEW_LIKE, post});
}).catch((err) => {
dispatch({ type: types.NEW_LIKE_ERROR, err});
});
};
};
In your data structure, you don't need to keep likes, as you can get it from likedBy. I'm not familiar with firestore api, but logic below should do what you want, you just need to get/set date to/from firestore
return (dispatch, getState, {getFirebase, getFirestore}) => {
const signedInUser = getState().firebase.auth;
const posts = ...// get current state of post, with likedBy field. Also, I assume here, that likedBy is always an array, with data or empty
let updatedLikedBy;
if(posts.likedBy.indexOf(signedInUser) ===-1){ //assuming that signedInUser is a string
updatedLikedBy = [...post.likedBy, signedInUser]
} else {
updatedLikedBy = post.likedBy.filter(item=>item!==signedInUser)
}
const updatedPost = {likedBy: updatedLikedBy} // now you can send this to firestore, and fire actions after success or fail.
};
};
To get likes now you just need to do posts.likedBy.length

setState with value of previously returned custom hook

I couldn't find a similar question here, so here it goes:
I created a custom hook useBudget to fetch some data.
const initalState = {
budget_amount: 0,
};
const useBudget = (resource: string, type: string) => {
const [budgetInfo, setBudget] = useState(initalState);
useEffect(
() => {
(async (resource, type) => {
const response = await fetchBudgetInfo(resource, type);
setBudget(response);
})(resource, type);
}, []);
return [budgetInfo];
};
And on the component that uses that hook, I have something like this:
const [budgetInfo] = useBudget(resource, type);
const [budgetForm, setBudgetForm] = useState({ warningMsg: null, errorMsg: null, budget: budgetInfo.budget_amount });
The problem is: The initial state of this component does not update after the fetching. budget renders with 0 initially and keeps that way. If console.log(budgetInfo) right afterwards, the budget is there updated, but the state is not.
I believe that this is happening due to the asynchronicity right? But how to fix this?
Thanks!
I could get to a fix, however, I am not 100% that this is the best/correct approach. As far as I could get it, due to the asynchronicity, I am still reading the old state value, and a way to fix this would be to set the state inside useEffect. I would have:
const [budgetInfo] = useBudget(resource, type);
const [appState, setAppState] = useState({ budget: budgetInfo.budget_amount });
useEffect(
() => {
setAppState({ budget: budgetInfo.budget_amount });
}, [budgetInfo]);
But it's working now!
Working example: https://stackblitz.com/edit/react-wiewan?file=index.js
Effects scheduled with useEffect don’t block the browser from updating the screen - that's why 0 (initialState) is displayed on the screen. After the value is fetched, the component stays the same as there is no change in its own state (budgetForm).
Your solution updates component's state once budgetInfo is fetched hence triggering a re-render, which works but seems to be rather a workaround.
useBudget could be used on its own:
const useBudget = (resource, type) => {
const [budgetInfo, setBudget] = useState(initalState);
const fetchBudgetInfo = async () => {
const response = await (new Promise((resolve) => resolve({ budget_amount: 333 })))
setBudget(response)
}
useEffect(() => {
fetchBudgetInfo(resource, type)
}, [])
return budgetInfo
};
const App = props => {
const { budget_amount } = useBudget('ok', 'ok')
return (
<h1>{budget_amount}</h1>
);
}
fetchBudgetInfo is split out since if we make effect function async (useEffect(async () => { ... })), it will be implicitly returning a promise and this goes against its design - the only return value must be a function which is gonna be used for cleaning up. Docs ref
Alternatively, consider retrieving data without a custom hook:
const fetchBudgetInfo = async (resource, type) => {
const response = await fetch(resource, type)
return response
}
useEffect(() => {
const response = fetchBudgetInfo(resource, type)
setAppState((prevState) => { ...prevState, budget: response.budget_amount });
}, []);
Notably, we are manually merging old state with the new value, which is necessary if appState contains several values - that's because useState doesn't shallowly merge objects (as this.setState would) but instead completely replaces state variable. Doc ref
On a somewhat related note, there is nothing wrong with using object to hold state, however using multiple state variables offers few advantages - more precise naming and the ability to do individual updates.

Resources