Issue
I am working on a React Native application using Firebase for data storage. I have a two components, let's call it Post and CommentSheet, where I display posts along with their details and a bottom sheet to display and upload comments.
The problem arises when I attempt to post a comment for a post. Although I can successfully log the postInfo in the useEffect when it updates, I encounter an issue when calling a function (onUpload) from another component (commentSheet). After testing, the postInfo prop appears to be null when the function is invoked, however it is logged correctly when used in useEffect in either components.
Here's a simplified flow of my code:
- The openCommentSheet function is called from a child component (Post) when a user clicks on a comment icon.
- getPostDetails fetches post information using onSnapshot and setPostInfo is called to store information.
- postInfo is passed to commentSheet component in-order to perform its own functions.
- Despite logging the correct postInfo in the useEffect, when onUpload is invoked, the postInfo is null.
Here's what I think are the relevant snippets of code:
const Post = ({post, navigation, getPostDetails, openCommentSheet, closeCommentSheet}) => {
...
const PostFooter = ({handleGoing, post, openCommentSheet, getPostDetails, navigation}) => (
...
<TouchableOpacity
onPress={(() => {
openCommentSheet(post.postID) //This works fine
})}>
<Image style={styles.footerIcon}
source={require('../../assets/icons/comment.png')}>
</Image>
</TouchableOpacity>
...
}
const HomeScreen = ({navigation}) => {
const [postInfo, setPostInfo] = useState(null);
const [postID, setPostID] = useState(null);
const [comments, setComments] = useState(null);
const bottomSheetRef = useRef(null);
const handleSheetChanges = useCallback(index => {
console.log('handleSheetChanges', index);
{
index == -1
? (setComments(null), setPostInfo(null), setPostID(null))
: null;
}
}, []);
const handleClosePress = () => {
bottomSheetRef.current.close();
};
const handleOpenPress = () => {
bottomSheetRef.current.snapToIndex(1);
};
useEffect(() => {
console.log('Updated postInfo:', postInfo);
}, [postInfo]);
const openCommentSheet = async postID => {
try {
getPostDetails(postID);
getCommentDetails(postID);
handleOpenPress();
} catch (error) {
console.error('Error fetching post information:', error);
}
};
const getPostDetails = async postID => {
onSnapshot(doc(db, 'posts', postID), doc => {
// console.log("Current data: ", doc.data());
setPostInfo(doc.data());
});
};
const getCommentDetails = async postID => {
const commentsCollection = collection(db, 'posts', postID, 'comments');
const commentsQuery = query(
commentsCollection,
orderBy('createdAt', 'asc'),
);
onSnapshot(commentsQuery, snap => {
console.log(snap.docs.length);
setComments(
snap.docs.map(comment => ({id: comment.id, ...comment.data()})),
);
});
};
return (
<SafeAreaView style={styles.container}>
<Header navigation={navigation} />
<PostScreen
navigation={navigation}
openCommentSheet={openCommentSheet}
closeCommentSheet={handleClosePress}
/>
<BottomTabs navigation={navigation} icons={BottomTabIcons} />
<CommentSheet
bottomSheetRef={bottomSheetRef}
postInfo={postInfo}
comments={comments}
postID={postID}
handleSheetChanges={handleSheetChanges}
/>
</SafeAreaView>
);
};
...
export default HomeScreen;
// CommentSheet.js
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {Text, View, Pressable, StyleSheet, Alert} from 'react-native';
import BottomSheet, {
BottomSheetTextInput,
BottomSheetFlatList,
BottomSheetFooter,
BottomSheetBackdrop,
} from '@gorhom/bottom-sheet';
import {
addDoc,
collection,
collectionGroup,
doc,
onSnapshot,
serverTimestamp,
setDoc,
} from '@firebase/firestore';
import {Formik} from 'formik';
import {Divider} from 'react-native-elements';
import Comment from './Comment';
import {auth, db} from '../../firebase';
const CommentSheet = ({
bottomSheetRef,
handleSheetChanges,
postInfo,
comments,
}) => {
const loadingIndicator = (
<Text style={styles.headerText}>Loading comments...</Text>
);
const snapPoints = useMemo(() => ['25%', '50%', '75%', '100%'], []);
const [postData, setPostData] = useState({});
const renderBackdrop = useCallback(
props => (
<BottomSheetBackdrop
appearsOnIndex={0}
disappearsOnIndex={-1}
{...props}
/>
),
[],
);
const onUpload = async comment => {
const postID = postInfo.postID;
try {
const commentInput = {
user: auth.currentUser.displayName,
profile_picture: auth.currentUser.photoURL,
owner_uid: auth.currentUser.uid,
owner_email: auth.currentUser.email,
comment: comment,
createdAt: serverTimestamp(),
};
const commentRef = await addDoc(
collection(db, 'posts', postID, 'comments'),
commentInput,
);
const commentID = commentRef.id;
await setDoc(
doc(db, 'posts', postID, 'comments', commentID),
{commentID},
{merge: true},
);
console.log('Data Submitted');
} catch (error) {
Alert.alert('This is awkward...', error.message);
}
};
// useEffect(() => {
// console.log(postInfo);
// });
const clearTextInput = formik => {
formik.resetForm();
};
const renderFooter = useCallback(
props => (
<Formik
initialValues={{comment: ''}}
onSubmit={async (values, formik) => {
console.log('Before onUpload:', postInfo); // returns postInfo as null
await onUpload(values.comment).then(clearTextInput(formik)); // doesn't work :(
}}>
{({handleBlur, handleChange, handleSubmit, values}) => (
<>
<BottomSheetFooter
{...props}
// bottomInset={24}
>
<Divider />
<View style={styles.footerContainer}>
<BottomSheetTextInput
style={styles.input}
multiline={true}
maxLength={200}
placeholder="Add a comment..."
onChangeText={handleChange('comment')}
onBlur={handleBlur('comment')}
value={values.comment}
/>
<Pressable
style={({pressed}) => [
styles.footerButton,
{
backgroundColor: pressed ? '#1c414f' : '#52bce3',
opacity: values.comment.trim() === '' ? 0.5 : 1,
},
]}
onPress={handleSubmit}
disabled={values.comment.trim() === ''}>
<Text style={styles.buttonText}>Post</Text>
</Pressable>
</View>
</BottomSheetFooter>
</>
)}
</Formik>
),
[],
);
return (
<BottomSheet
ref={bottomSheetRef}
index={-1}
snapPoints={snapPoints}
onChange={handleSheetChanges}
enablePanDownToClose={true}
backdropComponent={renderBackdrop}
keyboardBlurBehavior="restore"
android_keyboardInputMode="adjustResize"
footerComponent={renderFooter}>
<View style={styles.contentContainer}>
<Text style={styles.commentHeader}>COMMENTS</Text>
{comments ? (
<>
<BottomSheetFlatList
data={comments}
renderItem={({item}) => (
<>
<Comment comment={item} />
</>
)}
/>
</>
) : (
loadingIndicator
)}
</View>
</BottomSheet>
);
};
...
export default CommentSheet;
Here are all the relevant console.logs:
LOG Updated postInfo: null
LOG handleSheetChanges 1
LOG Updated postInfo: {"caption": "My nephew", "comments": [], "createdAt": {"nanoseconds": 574000000, "seconds": 1700339789}, "fileName": "rn_image_picker_lib_temp_abe4a703-c70b-462d-8ee3-120e404b21ab.jpg", "imageUrl": "https://firebasestorage.googleapis.com/v0/b/motive-8c0ca.appspot.com/o/OmWrXsgS9ibbuHATrP9AEYB9bvK2%2Fposts%2Frn_image_picker_lib_temp_abe4a703-c70b-462d-8ee3-120e404b21ab.jpg?alt=media&token=fe89366c-2465-4bdb-a292-f83b2030e878", "owner_email": "[email protected]", "owner_uid": "OmWrXsgS9ibbuHATrP9AEYB9bvK2", "postID": "AT1TYlXFLyB0MxoYcosX", "profile_picture": "https://firebasestorage.googleapis.com/v0/b/motive-8c0ca.appspot.com/o/OmWrXsgS9ibbuHATrP9AEYB9bvK2%2Fpfp?alt=media&token=1f62f214-99ed-44b0-af06-af83cacee6ed", "user": "naechebango", "users_going": []}
LOG 3
LOG Before onUpload: null
Initially postInfo is null, the comment sheet is opened, useEffect updates it, but it still says no when trying to upload a comment in the commentSheet component.
For some reason it gets fixed after making any change in the code and saving the code. I assume it's because the component gets re-rendered with a full postInfo object already initialised.
Comments are displayed as intended so I know it's not an issue of data retrieval.
I am using gorhom's bottom sheet for the comment components if that helps.
Solution
Taking a look at where your "Before upload" log is:
const renderFooter = useCallback(
props => (
<Formik
initialValues={{comment: ''}}
onSubmit={async (values, formik) => {
console.log('Before onUpload:', postInfo);
await onUpload(values.comment).then(clearTextInput(formik));
}}>
{ /* ... */ }
</Formik>
),
[],
);
Because you've used the useCallback
hook and passed in []
as the second argument, you've effectively instructed React to create your callback, but only use the callback generated on the first render.
Taking a look at your useState calls for postInfo
:
const [postInfo, setPostInfo] = useState(null);
We can see that on your first render, postInfo
will be set to null
, and in turn that value is used in the returned renderFooter
callback when it is parsed for the first time.
On later renders, that callback is regenerated with the new value, but because the callback is already defined, the fresh callback is discarded because none of the properties passed in as the second argument changed. To have React use the new value whenever postInfo
changes, you need to pass in [postInfo]
as the second argument to useCallback
.
This results in the following:
// renderFooter is now changed whenever postInfo changes
const renderFooter = useCallback(
props => (
<Formik
initialValues={{comment: ''}}
onSubmit={async (values, formik) => {
console.log('Before onUpload:', postInfo);
await onUpload(values.comment).then(clearTextInput(formik));
}}>
{ /* ... */ }
</Formik>
),
[postInfo] // <-- use new value when postInfo changes
);
It's then up to your BottomSheet component to handle rendering the new footer.
Answered By - samthecodingman
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.