I'm trying to get my head around Firestore/Reactfire so am working on a basic todo app to help me grasp the concepts.
I have a screen /Screens/Mainscreen.js that loads all of a user's todo lists. There's an input at the top and a button to add the new list. This all works, except the new list isn't loaded onto the screen? If I refresh the screen, the new list is loaded then. It seems that I've missed some part of how Reactfire works, but my code seems to follow all of the demo docs so I'm not sure what I'm doing wrong.
Full WIP project here: https://github.com/warm--tape/todo/ (add a .env with Firebase credentials in if desired)
Here's my MainScreen.js:
function MainScreen({ navigation }) {
// Logout action. Probably move.
const auth = useAuth();
function handleLogOut() {
auth.signOut().then(() => navigation.replace('AuthScreen'));
}
// Load User
const { status: userStatus, data: user } = useUser();
const { uid } = user;
// Load User Lists from Firestore
const firestore = useFirestore();
const listCollection = collection(firestore, 'lists');
const userListQuery = query(listCollection, where('access', 'array-contains', uid || 0));
const { status: listStatus, data: rawListData } = useFirestoreCollectionData(userListQuery, { idField: 'id' });
// Load toast
const toast = useToast();
// Set up state
const [isLoading, setIsLoading] = useState(true);
const [listData, setListData] = useState([]);
const [listToAdd, setListToAdd] = useState({ listName: '' });
const [itemToAdd, setItemToAdd] = useState({ itemName: '' });
const [errors, setErrors] = useState({});
// Hide screen until loaded
if (isLoading && userStatus === 'success' && listStatus === 'success') {
setListData(rawListData);
setIsLoading(false);
}
// Form Validator
const validate = () => {
// Currently no validation
return true;
};
// Handle add list
async function onAddList() {
if (validate()) {
delete listToAdd.NO_ID_FIELD;
const listToAddData = { ...listToAdd, ...{ owner: uid, access: [uid] } };
await addDoc(collection(firestore, "lists"), listToAddData).then(() => {
toast.show({
title: "List Added",
placement: "bottom"
});
setListToAdd({ listName: '' });
});
} else {
alert('Validation Failed')
}
};
// Handle press on list
function onPressListHandler(id) {
navigation.navigate('ListDetailScreen', {listId: id})
}
// =========================================================================
// Render loading spinner
if (isLoading) {
return (
<LoadingSpinner />
);
}
// =========================================================================
// Render
const listRenderItem = ({ item }) => (
<Pressable
onPress={()=>{onPressListHandler(item.id)}}
borderBottomWidth="1" _dark={{borderColor: "gray.600"}} borderColor="coolGray.200" pl="4" pr="5" py="2"
>
<HStack space={3} justifyContent="start" alignItems="center">
<IconButton variant="unstyled" icon={<Icon as={Ionicons} name="list-outline" size="sm" />} onPress={() => {}} />
<Text _dark={{color: "warmGray.50"}} color="coolGray.800">
{item.listName}
</Text>
</HStack>
</Pressable>
);
// =========================================================================
// Render
return (
<ScreenWrapper>
<HStack>
<FormControl isRequired isInvalid={'listName' in errors}>
<Input placeholder="Add List..." value={listToAdd.listName} onChangeText={value => setListToAdd({ ...listToAdd, ...{ listName: value } })} />
{'listName' in errors ? <FormControl.ErrorMessage>{errors.listName}</FormControl.ErrorMessage> : null}
</FormControl>
<Button onPress={() => { onAddList() }} >
<Icon color="white" as={Ionicons} name="add" size="sm" />
</Button>
</HStack>
<Divider my="3" />
<Box>
<FlatList
data={listData}
renderItem={listRenderItem}
keyExtractor={item => item.id} />
</Box>
<Divider my="3" />
<Button onPress={() => handleLogOut()}>Logout</Button>
</ScreenWrapper>
);
}
export default MainScreen;
Turns out I had structured my code badly, so after reviewing a few demos and the docs in depth, the following refactor works:
import React, { useState } from 'react';
import { useFirestore, useUser, useFirestoreCollectionData } from 'reactfire';
import { addDoc, collection, query, where } from 'firebase/firestore';
import {
Text,
Button,
FlatList,
HStack,
Pressable,
Icon,
FormControl,
Input,
IconButton,
useToast,
Divider
} from 'native-base';
import { Ionicons } from '@expo/vector-icons';
import ScreenWrapper from '../components/ScreenWrapper';
import LoadingSpinner from '../components/LoadingSpinner';
function onPressListHandler(id) {
navigation.navigate('ListDetailScreen', { listId: id })
}
const listRenderItem = ({ item }) => (
<Pressable
onPress={() => { onPressListHandler(item.id) }}
borderBottomWidth="1" _dark={{ borderColor: "gray.600" }} borderColor="coolGray.200" pl="4" pr="5" py="2"
>
<HStack space={3} justifyContent="start" alignItems="center">
<IconButton variant="unstyled" icon={<Icon as={Ionicons} name="list-outline" size="sm" />} onPress={() => { }} />
<Text _dark={{ color: "warmGray.50" }} color="coolGray.800">
{item.listName}
</Text>
</HStack>
</Pressable>
);
const ListsList = () => {
// Load User
const { status: userStatus, data: user } = useUser();
const { uid } = user;
// Load User Lists from Firestore
const firestore = useFirestore();
const listCollection = collection(firestore, 'lists');
const userListQuery = query(listCollection, where('access', 'array-contains', uid || 0));
const { status: listStatus, data: lists } = useFirestoreCollectionData(userListQuery, { idField: 'id' });
// Loading lists
if (userStatus === 'loading' || listStatus === 'loading') {
return <LoadingSpinner />;
}
// Render
return (
<>
<FlatList
data={lists}
renderItem={listRenderItem}
keyExtractor={item => item.id}
/>
</>
);
};
function MainScreen({ navigation }) {
// Load toast
const toast = useToast();
const [listToAdd, setListToAdd] = useState({ listName: '' });
const [errors, setErrors] = useState({});
// Form Validator
const validate = () => {
// None needed here.
return true;
};
// Handle add list
async function onAddList() {
if (validate()) {
delete listToAdd.NO_ID_FIELD;
const listToAddData = { ...listToAdd, ...{ owner: uid, access: [uid] } };
await addDoc(collection(firestore, "lists"), listToAddData).then(() => {
toast.show({
title: "List Added",
placement: "bottom"
});
setListToAdd({ listName: '' });
});
} else {
alert('Validation Failed')
}
};
// =========================================================================
// Render
return (
<ScreenWrapper>
<HStack>
<FormControl isRequired isInvalid={'listName' in errors}>
<Input placeholder="Add List..." value={listToAdd.listName} onChangeText={value => setListToAdd({ ...listToAdd, ...{ listName: value } })} />
{'listName' in errors ? <FormControl.ErrorMessage>{errors.listName}</FormControl.ErrorMessage> : null}
</FormControl>
<Button onPress={() => { onAddList() }} >
<Icon color="white" as={Ionicons} name="add" size="sm" />
</Button>
</HStack>
<Divider my="3" />
<ListsList />
</ScreenWrapper>
);
}
export default MainScreen;